# ReFormer CORE - LLM Integration Guide
# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.

> Reactive form state management library for React with signals-based architecture
> Package: @reformer/core  •  Version: 5.0.2

## Table of Contents
- 01-api-reference.md — api-reference
- 02-quick-start.md — quick-start
- 03-api-signatures.md — api-signatures
- 04-common-patterns.md — common-patterns
- 05-common-mistakes.md — common-mistakes
- 06-troubleshooting.md — troubleshooting
- 07-complete-import.md — complete-import
- 08-form-types.md — form-types
- 09-formschema.md — formschema
- 10-arrays.md — arrays
- 11-async-watchfield.md — async-watchfield
- 12-array-cleanup.md — array-cleanup
- 13-multi-step.md — multi-step
- 14-extended-mistakes.md — extended-mistakes
- 15-project-structure.md — project-structure
- 16-ui-components.md — ui-components
- 17-nonexistent-api.md — nonexistent-api
- 19-reading-values.md — reading-values
- 20-compute-vs-watch.md — compute-vs-watch
- 21-array-operations.md — array-operations
- 22-cycle-detection.md — cycle-detection
- 23-copy-from.md — copyFrom — Копирование значений между полями
- 24-sync-fields.md — syncFields — Двусторонняя синхронизация полей
- 25-reset-when.md — resetWhen — Условный сброс полей
- 26-transform-value.md — transformValue — Автоматическая трансформация значений
- 27-revalidate-when.md — revalidateWhen — Перевалидация полей по триггерам
- 28-submit-and-reset.md — Submit и Reset — Жизненный цикл отправки формы
- 29-async-preload.md — Async Preload — Загрузка начальных значений и динамических справочников
- 30-type-safety-recipes.md — type-safety-recipes
- 31-async-validator-debounce.md — async-validator-debounce
- 32-async-options-loading.md — async-options-loading
- API Reference (auto-generated from JSDoc)

## 1. 1. API Reference

### Imports (CRITICALLY IMPORTANT)

| What                                                                                        | Where                       |
| ------------------------------------------------------------------------------------------- | --------------------------- |
| `createForm`, `useFormControl`, `useFormControlValue`, `validateForm`                       | `@reformer/core`            |
| `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `FormProxy`, `FieldNode` | `@reformer/core`            |
| `FormSchema`, `FieldConfig`, `ArrayNode`                                                    | `@reformer/core`            |
| `required`, `min`, `max`, `minLength`, `maxLength`, `email`                                 | `@reformer/core/validators` |
| `pattern`, `url`, `phone`, `isNumber`, `integer`, `multipleOf`, `nonNegative`, `nonZero`    | `@reformer/core/validators` |
| `validate`, `validateAsync`, `applyWhen`, `apply`                                           | `@reformer/core/validators` |
| `notEmpty`, `validateItems`                                                                 | `@reformer/core/validators` |
| `computeFrom`, `enableWhen`, `disableWhen`, `watchField`, `copyFrom`                        | `@reformer/core/behaviors`  |
| `resetWhen`, `revalidateWhen`, `syncFields`                                                 | `@reformer/core/behaviors`  |
| `transformValue`, `transformers`                                                            | `@reformer/core/behaviors`  |

### Type Values

- Optional numbers: `number | undefined` (NOT `null`)
- Optional strings: `string` (empty string by default)
- Do NOT add `[key: string]: unknown` to form interfaces

### React Hooks Comparison (CRITICALLY IMPORTANT)

| Hook | Return Type | Subscribes To | Use Case |
|------|-------------|---------------|----------|
| `useFormControl(field)` | `{ value, errors, disabled, touched, ... }` | All signals | Full field state, form inputs |
| `useFormControlValue(field)` | `T` (value directly) | Only value signal | Conditional rendering |

**CRITICAL**: Do NOT destructure `useFormControlValue`! It returns `T` directly, NOT `{ value: T }`.

```typescript
// WRONG - will always be undefined!
const { value: loanType } = useFormControlValue(control.loanType);

// CORRECT
const loanType = useFormControlValue(control.loanType);

// CORRECT - useFormControl returns object, destructuring OK
const { value, errors, disabled } = useFormControl(control.loanType);
```

## 2. 1.5 QUICK START - Minimal Working Form

> **Schema-driven UI rule (read first)**: компонент И его пропсы (label, placeholder,
> options, type) объявляются в **схеме поля** (`component` + `componentProps`).
> В JSX рендерится один универсальный `<FormField control={form.x} />` из
> `@reformer/ui-kit` БЕЗ дополнительных props. Не пиши свои `Input`/`Select`/
> `Checkbox`-обёртки с `label`-prop'ами — это anti-pattern. См.
> `find_recipe(package="@reformer/ui-kit", topic="form-field-integration")`.

```typescript
import { createForm, type FormProxy, type FormSchema } from '@reformer/core';
import { required, email } from '@reformer/core/validators';
import { FormField, Input, Button } from '@reformer/ui-kit';

// 1. Define form type as `type` alias (not `interface` — see Recipe 2)
type ContactForm = {
  name: string;
  email: string;
};

// 2. Schema: component + componentProps decl in fields, no JSX label props
const form = createForm<ContactForm>({
  form: {
    name: {
      value: '',
      component: Input,
      componentProps: { label: 'Name', placeholder: 'Your name' },
    },
    email: {
      value: '',
      component: Input,
      componentProps: { label: 'Email', type: 'email' },
    },
  } satisfies FormSchema<ContactForm>,
  validation: (path) => {
    required(path.name, { message: 'Name is required' });
    required(path.email, { message: 'Email is required' });
    email(path.email, { message: 'Invalid email format' });
  },
});

// 3. Use in React component — thin JSX, FormField does ALL heavy lifting
function ContactFormComponent() {
  const handleSubmit = async () => {
    await form.submit((values: ContactForm) => {
      console.log('Form submitted:', values);
    });
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <FormField control={form.name} testId="name" />
      <FormField control={form.email} testId="email" />
      <Button type="submit">Send</Button>
    </form>
  );
}

// 4. Pass form to child components via props (NOT context!)
type FormStepProps = {
  form: FormProxy<ContactForm>;
};

function FormStep({ form }: FormStepProps) {
  return <FormField control={form.name} testId="name" />;
}
```

### Arrays of objects — tuple format

⚠️ Массивы объектов в схеме объявляются **через tuple `[itemSchema]`**, НЕ через `FieldConfig` с `value: []`:

```typescript
// ❌ DON'T — TS error: Type 'FieldConfig<PropertyItem[]>' is not assignable to type '[FormSchema<PropertyItem>]'
const schema: FormSchema<{ properties: PropertyItem[] }> = {
  properties: { value: [], component: Input },  // ← intuitive but WRONG
};

// ✅ DO — tuple [itemSchema]: ArrayNode infers item shape from первого элемента
const schema: FormSchema<{ properties: PropertyItem[] }> = {
  properties: [
    {
      type: { value: 'apartment', component: Select, componentProps: { ... } } satisfies FieldConfig<PropertyType>,
      description: { value: '', component: Textarea, componentProps: { label: 'Описание' } },
      estimatedValue: { value: 0, component: Input, componentProps: { label: 'Стоимость', type: 'number' } },
    },
  ],
};
```

В runtime массив пуст (`form.properties.value === []`); tuple — это **template** для каждого item, который применяется при `form.properties.push()` / `.insert()`. См. подробности в `find_recipe(topic="form-array")` и `find_recipe(topic="array-operations")`.

### When to write your own field components (advanced — rare)

Свои компоненты нужны ТОЛЬКО если:

- ты намеренно избегаешь `@reformer/ui-kit` (например, проект уже имеет свою design system)
- нужен особый низкоуровневый input, который не покрывается `FormField` + `componentProps`

В этом случае см. секцию `## 14.5 UI COMPONENT PATTERNS` ниже — но даже там
паттерн **schema-driven** (label/options не из JSX-props, а из `componentProps` через
`useFormControl(...).componentProps`).

## 3. 2. API SIGNATURES

### Validators

Operators register validators in the schema. Validator factories return pure
`Validator<TForm, TField>` functions of shape `(value, control, root) => ValidationError | null`.

```typescript
// Operators (register validators in schema)
validate(path, validator: Validator<TForm, TField>, options?: { message?: string })
validateAsync(path, validator: AsyncValidator<TForm, TField>, options?: { message?: string; debounce?: number })

// Validator factories (return Validator). Pass to validate().
required(options?: { message?: string })
min(value: number, options?: { message?: string })
max(value: number, options?: { message?: string })
minLength(length: number, options?: { message?: string })
maxLength(length: number, options?: { message?: string })
email(options?: { message?: string })
pattern(regex: RegExp, options?: { message?: string })
url(options?: { message?: string; requireProtocol?: boolean })
phone(options?: { message?: string; format?: PhoneFormat })
// Number validator factories (atomic — compose via several validate() calls)
isNumber(options?: { message?: string })
integer(options?: { message?: string })
multipleOf(divisor: number, options?: { message?: string })
nonNegative(options?: { message?: string })       // value >= 0
nonZero(options?: { message?: string })           // value !== 0
date(options?: { message?: string; minAge?: number; maxAge?: number; noFuture?: boolean; noPast?: boolean })
notEmpty(options?: { message?: string })

// Usage: pass a factory result to validate()
validate(path.email, required());
validate(path.age, min(18));
validate(path.amount, integer());
validate(path.amount, min(0));
validate(path.amount, max(1000));

// Conditional validation (3 arguments!)
applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)

// applyWhen Examples - CRITICALLY IMPORTANT
// applyWhen takes 3 arguments: triggerField, condition, validators

// Example 1: Simple condition
applyWhen(
  path.loanType,                    // 1st: field to watch
  (type) => type === 'mortgage',    // 2nd: condition on field value
  (p) => {                          // 3rd: validators to apply
    validate(p.propertyValue, required());
    validate(p.propertyValue, min(100000));
  }
);

// Example 2: Nested field as trigger
applyWhen(
  path.address.country,
  (country) => country === 'US',
  (p) => {
    validate(p.address.state, required());
    validate(p.address.zip, pattern(/^\d{5}(-\d{4})?$/));
  }
);

// Example 3: Boolean trigger
applyWhen(
  path.hasInsurance,
  (has) => has === true,
  (p) => {
    validate(p.insuranceCompany, required());
    validate(p.policyNumber, required());
  }
);

// WRONG - only 2 arguments (React Hook Form pattern)
applyWhen(
  (form) => form.loanType === 'mortgage',  // WRONG!
  () => { validate(path.propertyValue, required()); }
);

// Array item schema operator
validateItems(arrayPath, itemValidatorsFn: (itemPath) => void)
```

### Behaviors

```typescript
// Enable/disable fields conditionally
enableWhen(path, condition: (form) => boolean, options?: { resetOnDisable?: boolean })
disableWhen(path, condition: (form) => boolean)

// Computed fields (same nesting level)
computeFrom(sourcePaths[], targetPath, compute: (values) => result, options?: { debounce?: number; condition?: (form) => boolean })

// Watch field changes (ALWAYS use { immediate: false } to prevent cycle detection!)
watchField(path, callback: (value, ctx: BehaviorContext) => void, options: { immediate: false; debounce?: number })

// Copy values between fields
copyFrom(sourcePath, targetPath, options?: { when?: (form) => boolean; fields?: string[]; transform?: (value) => value })

// Reset field when condition met
resetWhen(path, condition: (form) => boolean, options?: { toValue?: any })

// Re-validate target field when any trigger changes
revalidateWhen(targetPath, triggerPaths[], options?: { debounce?: number })

// Sync multiple fields
syncFields(paths[], options?: { bidirectional?: boolean })

// Transform values
transformValue(path, transformer: (value) => value, options?: { on?: 'change' | 'blur' })
transformers.trim, transformers.toUpperCase, transformers.toLowerCase, transformers.toNumber

// BehaviorContext interface:
interface BehaviorContext<TForm> {
  form: FormProxy<TForm>;            // Form proxy with typed field access
  setFieldValue: (path: string, value: any) => void;
  // To READ field values, use: ctx.form.fieldName.value.value
}
```

## 4. 3. COMMON PATTERNS

### Conditional Fields with Auto-Reset

```typescript
enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
  resetOnDisable: true,
});
```

### Computed Field from Nested to Root Level

```typescript
// DO NOT use computeFrom for cross-level computations
// Use watchField instead. Write API is ctx.form.<path>.setValue(value) —
// ctx.setFieldValue(name, value) does not exist.
watchField(
  path.nested.field,
  (value, ctx) => {
    if (ctx.form.rootField.value.value !== computedValue) {
      ctx.form.rootField.setValue(computedValue);
    }
  },
  { immediate: false }
);
```

### Validation callback canonical shape

**Default (recommended) — fully typed:** annotate the validation/behavior
function with `ValidationSchemaFn<T>` / `BehaviorSchemaFn<T>` (imported from
`@reformer/core`, NOT from `/validators` or `/behaviors`). `path` and inner
`applyWhen`-callback `p` are inferred automatically — no `any` casts needed.

```typescript
import {
  createForm,
  type FormProxy,
  type ValidationSchemaFn,
  type BehaviorSchemaFn,
} from '@reformer/core';
import { required, min, validate, applyWhen, validateItems } from '@reformer/core/validators';
import { computeFrom } from '@reformer/core/behaviors';

const validation: ValidationSchemaFn<MyForm> = (path) => {
  required(path.step1.loanAmount, { message: 'Введите сумму' });
  min(path.step1.loanAmount, 50000);

  validate(path.step4.workExperienceCurrent, (value, ctx) => {
    const total = ctx.form.step4.workExperienceTotal.value.value;
    if (total != null && value != null && value > total) {
      return { code: 'experience-mismatch', message: 'Текущий стаж не может превышать общий' };
    }
    return null;
  });

  // applyWhen-callback `p` is auto-typed as FieldPath<MyForm> via the signature
  // (path: FieldPath<TForm>) => void — no manual annotation needed.
  applyWhen(
    path.step1.loanType,
    (loanType) => loanType === 'mortgage',
    (p) => {
      required(p.step1.propertyValue, { message: 'Введите стоимость недвижимости' });
      min(p.step1.propertyValue, 500000);
    }
  );

  validateItems(path.step5.properties, (itemPath) => {
    required(itemPath.type);
    min(itemPath.estimatedValue, 0);
  });
};

const behavior: BehaviorSchemaFn<MyForm> = (path) => {
  computeFrom(
    [path.step1.loanAmount],
    path.summary.total,
    ({ step1 }: MyForm) => step1.loanAmount ?? 0
  );
};

export const myForm: FormProxy<MyForm> = createForm({
  form: {
    /* schema */
  },
  validation,
  behavior,
});
```

**Legacy workaround — only when TS2589 hits.** For deeply-nested forms
(typically 6+ levels) the recursive `FormSchema<T>` mapped type can hit
TS2589 ("type instantiation excessively deep"). Only then fall back to a
function-cast on the `createForm` config plus `(path: any)` annotations.
This **disables** type-checking inside the callback — use it only when the
canonical shape literally won't compile, not as a default.

```typescript
// LEGACY — apply ONLY if canonical shape produces TS2589
export const deepForm: FormProxy<DeepForm> = (
  createForm as (config: { form: unknown; validation: unknown }) => FormProxy<DeepForm>
)({
  form: {
    /* very deep schema */
  },
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  validation: (path: any) => {
    required(path.a.b.c.d.e.field);
    applyWhen(
      path.a.b,
      (form) => form.a.b.flag,
      (p: typeof path) => {
        // (p: typeof path) preserves field-level type-checking inside the block
        required(p.a.b.x);
      }
    );
  },
});
```

Form-shape `interface` types may need to be converted to `type` aliases for
structural compatibility with `Record<string, FormValue>`-constrained
generics like `ArrayNode<T>` / `FormProxy<T>` (interface lacks an implicit
index signature). See `30-type-safety-recipes.md`.

### Type-Safe useFormControl

```typescript
const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
```

### Validation Priority (IMPORTANT)

**Operators register validators; built-in factories return validators. Pass factories to `validate()`.**

Operators: `validate`, `validateAsync`, `applyWhen`, `apply`, `validateItems`.
Factories (return `Validator<TForm, TField>`): `required`, `email`, `min`, `max`, `minLength`, `maxLength`,
`pattern`, `url`, `phone`, `number`, `date`, `notEmpty`.

```typescript
// 1. BEST: Pass built-in factories to validate()
validate(path.email, required());
validate(path.email, email());
validate(path.age, min(18));
validate(path.password, minLength(8));
validate(path.phone, pattern(/^\+7\d{10}$/));

// 2. GOOD: Custom validator when no factory fits. Signature: (value, control, root).
validate(path.customField, (value, control, root) => {
  if (customCondition(value)) {
    return { code: 'custom', message: 'Custom error' };
  }
  return null;
});

// 3. WRONG: Don't recreate built-in validators inline
validate(path.email, (value) => {
  if (!value) return { code: 'required', message: 'Required' }; // Use required() instead!
  if (!value.includes('@')) return { code: 'email', message: 'Invalid' }; // Use email() instead!
  return null;
});
```

## 5. TYPED schema generic + extracted callback rules

Two rules apply to **every** validation/behavior schema you write:

### Rule A — Use the form interface as `<T>` in the schema generic; never `any`

```typescript
import type { CreditApplicationForm } from './types';

// ✅ generic fixed → path / ctx / value all infer correctly
const validation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
  required(path.email);
  validateTree<CreditApplicationForm>((ctx) => {
    const form = ctx.form.getValue();  // typed CreditApplicationForm
    if (form.loanAmount && form.totalIncome && form.loanAmount > form.totalIncome * 12 * 10) {
      return { code: 'loanCap', message: '...' };
    }
    return null;
  }, { targetField: 'loanAmount' });
};

const behavior: BehaviorSchemaFn<CreditApplicationForm> = (path) => {
  computeFrom([path.price, path.quantity], path.total, (values) => values.price * values.quantity);
};

// ❌ generic dropped or path: any — silent fail on field-name typos
const validation: ValidationSchemaFn<any> = (path: any) => { ... };
const behavior = (path: any) => { ... };  // missing BehaviorSchemaFn<T> annotation entirely
```

`as any` cast is acceptable in narrow call-sites (e.g. TS2589 workaround for very deep forms) but should be **scoped to the expression**, not the entire callback parameter.

### Rule B — Inline OK for short callbacks; extract module-level for content

**Inline acceptable** (1-3 line callbacks):

```typescript
applyWhen(
  path.loanType,
  (t) => t === 'mortgage',
  (p) => required(p.propertyValue)
);
enableWhen(path.discountCode, (form) => form.subtotal > 100);
copyFrom(path.regAddress, path.resAddress, { when: (form) => form.sameAsReg, fields: 'all' });
validate(path.agree, (v: boolean) => (v === true ? null : { code: 'mustAgree', message: '...' }));
```

**Extract module-level when**:

- callback >5 lines or has multiple return branches
- `computeFrom([...], target, callback)` — inline arrow may lose `(values: TForm)` inference and force `(values: any)`. Module-level `function computeX(form: T): R` infers correctly.
- async `watchField` with try/catch
- callback reused in multiple places
- cross-field `validateTree` with branching logic

```typescript
// ✅ extracted typed helpers — used by behavior schema
function computeMonthlyPayment(form: LoanForm): number {
  const P = form.loanAmount,
    n = form.loanTerm,
    annual = form.interestRate;
  if (!P || !n || !annual || P <= 0 || n <= 0) return 0;
  const i = annual / 100 / 12;
  if (i <= 0) return Math.round(P / n);
  const factor = Math.pow(1 + i, n);
  return Math.round((P * (i * factor)) / (factor - 1));
}

const behavior: BehaviorSchemaFn<LoanForm> = (path) => {
  computeFrom(
    [path.loanAmount, path.loanTerm, path.interestRate],
    path.monthlyPayment,
    computeMonthlyPayment // by-reference, signature already typed
  );
};
```

Reference patterns: `complex-multy-step-form/schemas/credit-application-behavior.ts`.

### Extracting Nested Rules

When the body of `applyWhen` / `validate` grows beyond a few lines,
extract it to a **named top-level function** typed with one of the public types from
`@reformer/core`:

- `ValidationSchemaFn<TForm>` — sub-schema passed to `applyWhen` or `apply`.
- `Validator<TForm, TField>` / `AsyncValidator<TForm, TField>` — field validator passed to `validate` / `validateAsync`. Cross-field правила пишутся в той же сигнатуре — `(value, control, root) => …` — и навешиваются на конкретное поле-носитель ошибки.

This keeps the schema body flat (reads like a table of contents) and surfaces the
**intent** of each rule via a meaningful name.

```typescript
import type { ValidationSchemaFn, Validator } from '@reformer/core';

// 1. Cross-field rule — extracted Validator (reads other fields via `root`)
const initialPaymentVsPropertyValue: Validator<CreditApplicationForm, unknown> = (
  _value,
  _control,
  root
) => {
  const form = root.getValue();
  if (form.initialPayment && form.propertyValue && form.initialPayment > form.propertyValue) {
    return { code: 'initialPaymentTooHigh', message: 'Взнос не может превышать стоимость' };
  }
  return null;
};

// 2. Custom field validator — extracted Validator
const validateAdultAge: Validator<CreditApplicationForm, string | undefined> = (value) => {
  if (!value) return null;
  const age = new Date().getFullYear() - new Date(value).getFullYear();
  if (age < 18) return { code: 'tooYoung', message: 'Минимум 18 лет' };
  return null;
};

// 3. Nested schema for applyWhen — extracted ValidationSchemaFn
const mortgageFieldsRules: ValidationSchemaFn<CreditApplicationForm> = (path) => {
  validate(path.propertyValue, required());
  validate(path.propertyValue, min(1000000));
  validate(path.initialPayment, required());
  validate(path.initialPayment, initialPaymentVsPropertyValue);
};

// 4. Main schema — flat, reads as a table of contents
export const basicInfoValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
  validate(path.loanType, required());
  validate(path.personalData.birthDate, validateAdultAge);
  applyWhen(path.loanType, (type) => type === 'mortgage', mortgageFieldsRules);
};
```

**Naming convention** (camelCase, semantic — not echoing the operator name):

- `applyWhen` sub-schema → describes the conditional branch: `mortgageFieldsRules`, `employedFieldsRules`.
- Cross-field `Validator` → describes the invariant: `initialPaymentVsPropertyValue`, `paymentToIncomeUnderHalf`.
- Field-level `Validator` → describes the check: `validateAdultAge`, `validatePasswordsMatch`.

**When to extract.** Inline lambdas are fine for short one-liners
(`applyWhen(path.x, (v) => v === 'mortgage', mortgageFieldsRules)` — the condition stays inline).
Extract whenever the body spans more than ~3 lines or contains nested rules.

## 6. 4. COMMON MISTAKES

### TSC overload-resolution error: `'form' does not exist in FormSchema<T>`

**Симптом**:

```
TS2769: Object literal may only specify known properties, and 'form' does not
exist in type 'FormSchema<MyForm>'.
```

(или похожее с `validation` / `behavior` ключами).

**Причина**: `createForm` имеет 2 overload — `GroupNodeConfig<T>` (с `form/validation/behavior`)
и schema-only. Если в inline literal `form: { ... }` есть глубокая inference-проблема
(чаще всего — literal default value у union-типа поля, см. Recipe 8 в
[30-type-safety-recipes.md](30-type-safety-recipes.md)), TS отбрасывает Overload 1 и
матчит Overload 2 (schema-only). В нём ключа `form` нет — получаешь misleading error.

**Workaround** — extract `form` literal в typed local:

```ts
const form: FormSchema<MyForm> = {
  fieldA: { value: 'foo', component: Input /* ... */ },
  // ...
};

createForm({ form, validation, behavior });
```

Теперь TS репортит **реальную** per-field ошибку — например:

```
TS2322: Type '"male"' is not assignable to type 'Gender | null'
```

После этого решай конкретную проблему (Recipe 8 — `satisfies FieldConfig<UnionType>`).

### Imports rule (#1 cause of cascading errors — read first)

Types live in `@reformer/core`. Functions live in submodules (`/validators`,
`/behaviors`). Mixing them produces TS2614 ("has no exported member"), which
in turn collapses ALL `(path) => ...` callbacks to `implicit any` and looks
like 30+ unrelated errors.

```typescript
// ❌ WRONG — TS2614 cascades to 30+ implicit-any errors below
import { type ValidationSchemaFn } from '@reformer/core/validators';
import { type BehaviorSchemaFn } from '@reformer/core/behaviors';

// ✅ CORRECT — types from main module, functions from submodules
import {
  type ValidationSchemaFn,
  type BehaviorSchemaFn,
  type FieldPath,
  type FormSchema,
  type FormProxy,
  createForm,
} from '@reformer/core';
import { required, min, max, applyWhen, apply } from '@reformer/core/validators';
import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
```

### useFormControlValue (CRITICAL)

```typescript
// WRONG - useFormControlValue returns T directly, NOT { value: T }
const { value: loanType } = useFormControlValue(control.loanType);
// Result: loanType is ALWAYS undefined! Conditional rendering will fail.

// CORRECT
const loanType = useFormControlValue(control.loanType);

// ALSO CORRECT - useFormControl returns object
const { value, errors } = useFormControl(control.loanType);
```

### Reading Field Values in BehaviorContext (CRITICAL)

```typescript
// WRONG - getFieldValue does NOT exist!
watchField(path.amount, (amount, ctx) => {
  const rate = ctx.getFieldValue('rate'); // ERROR: Property 'getFieldValue' does not exist
});

// CORRECT - use ctx.form.fieldName.value.value
watchField(path.amount, (amount, ctx) => {
  const rate = ctx.form.rate.value.value; // Read via signal
  ctx.setFieldValue('total', amount * rate);
});
```

### Validators

```typescript
// WRONG
required(path.email, 'Email is required');

// CORRECT
required(path.email, { message: 'Email is required' });
```

### Form-shape types — `type` over `interface`

```typescript
// ❌ WRONG — interface lacks implicit index signature; ArrayNode<T> /
//          FormArraySection<T> constraints (T extends FormFields) reject it.
//          Symptom: "Type 'X' is not assignable to type 'Partial<FormFields>'"
//          on FormArraySection.initialValue, OR "T does not satisfy
//          constraint 'FormFields'" on explicit generic.
export interface PropertyItem {
  type: PropertyType;
  description: string;
}

// ✅ CORRECT — type alias is structurally compatible with Record<string, FormValue>
export type PropertyItem = {
  type: PropertyType;
  description: string;
};
```

`number | null` (vs `number | undefined`) is fine — built-in validators
(`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`,
`maxAge`) accept `number | null | undefined` / `string | null | undefined`.

### computeFrom — type-safe callback (no `as` casts)

Annotate the destructured argument with the form type. Without annotation,
TS sees fields as `unknown` (`computeFn: (values: TForm) => T` infers the
full form, not a narrowed Pick of source fields), and you end up with
`as number | null` casts in every line.

```typescript
// ❌ WRONG — leads to `as` casts in every line
computeFrom([path.loanAmount, path.loanTerm], path.monthlyPayment, ({ loanAmount, loanTerm }) => {
  const a = (loanAmount as number | null) ?? 0;
  const t = (loanTerm as number | null) ?? 0;
  return annuityMonthly(a, t, 0.1);
});

// ✅ CORRECT — annotated destructuring, no casts, fields properly typed
computeFrom(
  [path.loanAmount, path.loanTerm],
  path.monthlyPayment,
  ({ loanAmount, loanTerm }: MyForm) => annuityMonthly(loanAmount ?? 0, loanTerm ?? 0, 0.1)
);
```

### computeFrom across nesting levels — use group-node subscription

```typescript
// ❌ WRONG — subscribing to leaf fields of a nested group leaks
//          implementation details and may fall back to FieldPath<unknown>
computeFrom(
  [path.personalData.firstName, path.personalData.lastName],
  path.fullName,
  ({ personalData }) => `${personalData?.firstName} ${personalData?.lastName}`
);

// ✅ CORRECT — subscribe to the group node itself; destructure the group
computeFrom([path.personalData], path.fullName, ({ personalData }: MyForm) =>
  [personalData.firstName, personalData.lastName].filter(Boolean).join(' ')
);
```

## 7. 5. TROUBLESHOOTING

| Error                                                  | Cause                                                    | Solution                                   |
| ------------------------------------------------------ | -------------------------------------------------------- | ------------------------------------------ |
| `'string' is not assignable to '{ message?: string }'` | Wrong validator format                                   | Use `{ message: 'text' }`                  |
| `'null' is not assignable to 'undefined'`              | Wrong optional type                                      | Replace `null` with `undefined`            |
| `FormFields[]` instead of concrete type                | Type inference issue                                     | Use `as FieldNode<T>`                      |
| `Type 'X' is missing properties from type 'Y'`         | Cross-level computeFrom                                  | Use watchField instead                     |
| `Module has no exported member`                        | Wrong import source                                      | Types from core, functions from submodules |
| `Cycle detected`                                       | Multiple watchers on same field calling disable/setValue | See 22-cycle-detection.md                  |

## 8. Import Patterns

```typescript
// Types - always from @reformer/core
import type {
  FormProxy,
  FieldNode,
  FieldPath,
  FormFields, // = Record<string, FormValue>
  FormValue, // primitive | nested | array element of any form
  FormSchema, // schema literal type for createForm<T>
  FieldConfig, // shape of a single field: { value, component, componentProps?, ... }
  ValidationSchemaFn,
  BehaviorSchemaFn,
} from '@reformer/core';

// Core functions and hooks
import {
  createForm,
  useFormControl,
  useFormControlValue,
  useArrayLength,
  useHiddenCondition,
  validateForm,
} from '@reformer/core';

// Validators - from /validators submodule
import { required, min, max, email, validate, applyWhen } from '@reformer/core/validators';

// Behaviors - from /behaviors submodule
import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
```

### Constraint to remember when typing nested forms

The proxy returned by `createForm<T>` and the `ArrayNode<U>` / `GroupNode<U>` types
require any form-shape generic to be assignable to `FormFields = Record<string, FormValue>`.
Plain interfaces work as long as every property is a valid `FormValue` (primitive,
nested object of `FormFields`, or array of `FormFields`). If TS complains
"Type 'X' does not satisfy the constraint 'FormFields'", add an index signature
or extend `FormFields` directly:

```typescript
interface AddressForm extends FormFields {
  street: string;
  city: string;
}

// or, if you can't extend (e.g. union), inline the index signature:
interface CoBorrower {
  fullName: string;
  phone: string;
  [key: string]: FormValue; // <- the constraint
}
```

## 9. 7. FORM TYPE DEFINITION

```typescript
// CORRECT form type definition
interface MyForm {
  // Required fields
  name: string;
  email: string;

  // Optional fields - use undefined, not null
  phone?: string;
  age?: number;

  // Enum/union types
  status: 'active' | 'inactive';

  // Nested objects
  address: {
    street: string;
    city: string;
  };

  // Arrays - use tuple format for schema
  items: Array<{
    id: string;
    name: string;
  }>;
}
```

## 10. 8. FORMSCHEMA FORMAT (CRITICALLY IMPORTANT)

**Every field MUST have `value` and `component` properties!**

### FieldConfig Interface

```typescript
interface FieldConfig<T> {
  value: T | null; // Initial value (REQUIRED)
  component: ComponentType; // React component (REQUIRED)
  componentProps?: object; // Props passed to component
  disabled?: boolean; // Disable field initially
  validators?: ValidatorFn[]; // Sync validators
  asyncValidators?: AsyncValidatorFn[]; // Async validators
  updateOn?: 'change' | 'blur' | 'submit';
  debounce?: number;
}
```

### Primitive Fields

```typescript
import { Input, Select, Checkbox } from '@/components/ui';

const schema: FormSchema<MyForm> = {
  // String field
  name: {
    value: '', // Initial value (REQUIRED)
    component: Input, // React component (REQUIRED)
    componentProps: {
      label: 'Name',
      placeholder: 'Enter name',
    },
  },

  // Number field (optional)
  age: {
    value: undefined, // Use undefined, NOT null
    component: Input,
    componentProps: { type: 'number', label: 'Age' },
  },

  // Boolean field
  agree: {
    value: false,
    component: Checkbox,
    componentProps: { label: 'I agree to terms' },
  },

  // Enum/Select field
  status: {
    value: 'active',
    component: Select,
    componentProps: {
      label: 'Status',
      options: [
        { value: 'active', label: 'Active' },
        { value: 'inactive', label: 'Inactive' },
      ],
    },
  },
};
```

### Nested Objects

```typescript
const schema: FormSchema<MyForm> = {
  address: {
    street: { value: '', component: Input, componentProps: { label: 'Street' } },
    city: { value: '', component: Input, componentProps: { label: 'City' } },
    zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
  },
};
```

### Arrays (Tuple Format)

```typescript
const itemSchema = {
  id: { value: '', component: Input, componentProps: { label: 'ID' } },
  name: { value: '', component: Input, componentProps: { label: 'Name' } },
};

const schema: FormSchema<MyForm> = {
  items: [itemSchema], // Array with ONE template item
};
```

### WRONG - This will NOT compile

```typescript
// Missing value and component - TypeScript will error!
const schema = {
  name: '', // Wrong
  email: '', // Wrong
};
```

### createForm API

```typescript
// Full config with behavior and validation
const form = createForm<MyForm>({
  form: formSchema, // Required: form schema with FieldConfig
  behavior: behaviorSchema, // Optional: behavior rules
  validation: validationSchema, // Optional: validation rules
});

// Access form controls
form.name.setValue('John');
form.address.city.value.value; // Get current value
form.items.push({ id: '1', name: 'Item' }); // Array operations
```

### createForm Returns a Proxy

```typescript
// createForm() returns FormProxy<T> (a Proxy wrapper around GroupNode)
// This enables type-safe field access:
const form = createForm<MyForm>({...});

form.email           // FieldNode<string> - TypeScript knows the type!
form.address.city    // FieldNode<string> - nested access works
form.items.at(0)     // FormProxy<ItemType> - array items

// IMPORTANT: Proxy doesn't pass instanceof checks!
// Use type guards instead:
import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';

if (isFieldNode(node)) { /* ... */ }   // Works with Proxy
if (node instanceof FieldNode) { /* ... */ } // Fails with Proxy!
```

## 11. 9. ARRAY SCHEMA FORMAT

**Array items are sub-forms!** Each array element is a complete sub-form with its own fields, validation, and behavior.

```typescript
// CORRECT - use tuple format for arrays
// The template item defines the sub-form schema for each array element
const itemSchema = {
  id: { value: '', component: Input },
  name: { value: '', component: Input },
  price: { value: 0, component: Input, componentProps: { type: 'number' } },
};

const schema: FormSchema<MyForm> = {
  items: [itemSchema], // Array of sub-forms
};
```

> **Type constraint:** the element interface used as `T` in `Array<T>` / `ArrayNode<T>`
> must be assignable to `FormFields = Record<string, FormValue>`. Either declare the
> item interface as `interface Item extends FormFields { … }` or add an explicit
> `[key: string]: FormValue` index signature. Without it TS reports
> _"Type 'Item' does not satisfy the constraint 'FormFields'"_ on `ArrayNode<Item>`.

> **Do NOT use `enableWhen(path.someArray, …, { resetOnDisable: true })` on a whole
> ArrayNode.** The combination triggers a reactive cycle on mount that prevents
> `DOMContentLoaded` (the browser hangs and has to be restarted). For
> "show array conditionally" — gate the rendering in JSX:
>
> ```tsx
> {
>   form.hasItems.value && <ArrayUI array={form.items} />;
> }
> ```
>
> `enableWhen` on individual `FieldNode` targets inside an array item template is fine.

```typescript
// Each array item is a GroupNode (sub-form) with its own controls:
form.items.map((item) => {
  // item is a sub-form (GroupNode) - access fields like nested form
  item.name.setValue('New Name');
  item.price.value.value; // Get current value
});
```

### Add/remove items — `initialValue` MUST be PLAIN leaf values

`FormArray.AddButton initialValue` (and the imperative `array.push(...)` /
`array.add(...)`) expect a payload of **plain leaf values**, NOT a fresh
FieldConfig template (`{ value, component, componentProps }`).

If you pass FieldConfig objects, the runtime stores them verbatim into the
new sub-form fields and silently breaks rendering: text fields show
`[object Object]`, checkboxes flip to truthy `true`, selects show empty.

```typescript
// ❌ WRONG — produces [object Object] in textareas, checkbox auto-true
const wrongPropertyTemplate = () => ({
  type: { value: 'apartment', component: Select, componentProps: { /* ... */ } },
  description: { value: '', component: Textarea, componentProps: { rows: 2 } },
  estimatedValue: { value: 0, component: Input },
  hasEncumbrance: { value: false, component: Checkbox },
});

// ✅ RIGHT — plain leaf values matching the item interface
const propertyTemplate = (): PropertyItem => ({
  type: 'apartment',
  description: '',
  estimatedValue: 0,
  hasEncumbrance: false,
});

// Either with the compound:
<FormArray.AddButton initialValue={propertyTemplate()} />

// Or imperative:
form.step5.properties.push(propertyTemplate());
```

The FieldConfig SHAPE (with `component`, `componentProps`, etc.) belongs
ONLY in the _initial_ schema literal passed to `createForm({...})`. New
items pushed at runtime reuse the same component+props that the schema's
template item declared — `initialValue` only fills the field VALUES.

> **Symptom checklist** (if you see these, you're passing FieldConfig):
>
> - `[object Object]` rendered in a Textarea/Input.
> - Boolean checkbox shows checked even though `value: false` was provided.
> - Select shows empty placeholder even though `value: 'apartment'` was provided.

```typescript
// WRONG - object format is NOT supported
const schema = {
  items: { schema: itemSchema, initialItems: [] }, // This will NOT work
};
```

### Array Item as Sub-Form

```typescript
// Validation for array items (each item is a sub-form)
validateItems(path.items, (itemPath) => {
  // itemPath provides paths to sub-form fields
  required(itemPath.name);
  min(itemPath.price, 0);
});

// Render array items - each item is a sub-form
{form.items.map((item, index) => (
  <div key={item.id}>
    {/* item is a sub-form - use FormField for each field */}
    <FormField control={item.name} />
    <FormField control={item.price} />
    <button onClick={() => form.items.removeAt(index)}>Remove</button>
  </div>
))}
```

## 12. 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)

```typescript
// CORRECT - async watchField with ALL safeguards
watchField(
  path.parentField,
  async (value, ctx) => {
    if (!value) return; // Guard clause

    try {
      const { data } = await fetchData(value);
      ctx.form.dependentField.updateComponentProps({ options: data });
    } catch (error) {
      console.error('Failed:', error);
      ctx.form.dependentField.updateComponentProps({ options: [] });
    }
  },
  { immediate: false, debounce: 300 } // REQUIRED options
);

// WRONG - missing safeguards
watchField(path.field, async (value, ctx) => {
  const { data } = await fetchData(value); // Will fail silently!
});
```

### Required Options for async watchField:

- `immediate: false` - prevents execution during initialization
- `debounce: 300` - prevents excessive API calls (300-500ms recommended)
- Guard clause - skip if value is empty
- try-catch - handle errors explicitly

## 13. 11. ARRAY CLEANUP PATTERN

```typescript
// CORRECT - cleanup array when checkbox unchecked
watchField(
  path.hasItems,
  (hasItems, ctx) => {
    if (!hasItems && ctx.form.items) {
      ctx.form.items.clear();
    }
  },
  { immediate: false }
);

// WRONG - no immediate: false, no null check
watchField(path.hasItems, (hasItems, ctx) => {
  if (!hasItems) ctx.form.items.clear(); // May crash on init!
});
```

## 14. 12. MULTI-STEP FORM VALIDATION

```typescript
// Step-specific validation schemas
const step1Validation: ValidationSchemaFn<Form> = (path) => {
  required(path.loanType);
  required(path.loanAmount);
};

const step2Validation: ValidationSchemaFn<Form> = (path) => {
  required(path.personalData.firstName);
  required(path.personalData.lastName);
};

// STEP_VALIDATIONS map for useStepForm hook
export const STEP_VALIDATIONS = {
  1: step1Validation,
  2: step2Validation,
};

// Full validation (combines all steps)
export const fullValidation: ValidationSchemaFn<Form> = (path) => {
  step1Validation(path);
  step2Validation(path);
};

// Using validateForm() for step validation
import { validateForm } from '@reformer/core';

const goToNextStep = async () => {
  const currentValidation = STEP_VALIDATIONS[currentStep];
  const isValid = await validateForm(form, currentValidation);

  if (!isValid) {
    form.markAsTouched(); // Show errors on current step fields
    return;
  }

  setCurrentStep(currentStep + 1);
};

// Full form submit with all validations
const handleSubmit = async () => {
  const isValid = await validateForm(form, fullValidation);

  if (isValid) {
    await form.submit(onSubmit);
  }
};
```

### Multi-Step Component Example

```tsx
function MultiStepForm() {
  const [step, setStep] = useState(1);

  const nextStep = async () => {
    const validation = STEP_VALIDATIONS[step];
    if (await validateForm(form, validation)) {
      setStep(step + 1);
    } else {
      form.markAsTouched();
    }
  };

  return (
    <div>
      {step === 1 && <Step1Fields form={form} />}
      {step === 2 && <Step2Fields form={form} />}

      <button onClick={() => setStep(step - 1)} disabled={step === 1}>
        Back
      </button>
      <button onClick={step === 2 ? handleSubmit : nextStep}>
        {step === 2 ? 'Submit' : 'Next'}
      </button>
    </div>
  );
}
```

## 15. 13. EXTENDED COMMON MISTAKES

### Behavior Composition (Cycle Error)

```typescript
// WRONG - apply() in behavior causes "Cycle detected"
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
  apply(addressBehavior, path.address); // WILL FAIL!
};

// CORRECT - inline or use setup function
const setupAddressBehavior = (path: FieldPath<Address>) => {
  watchField(
    path.region,
    async (region, ctx) => {
      // ...
    },
    { immediate: false }
  );
};

const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
  setupAddressBehavior(path.address); // Works!
};
```

### Infinite Loop in watchField

```typescript
// WRONG - causes infinite loop
watchField(path.field, (value, ctx) => {
  ctx.form.field.setValue(value.toUpperCase()); // Loop!
});

// CORRECT - write to different field OR add guard
watchField(
  path.input,
  (value, ctx) => {
    const upper = value?.toUpperCase() || '';
    if (ctx.form.display.value.value !== upper) {
      ctx.form.display.setValue(upper);
    }
  },
  { immediate: false }
);
```

### Multiple Watchers on Same Field (Cycle Error)

```typescript
// WRONG - multiple watchers on insuranceType + missing { immediate: false }
watchField(path.insuranceType, (_, ctx) => {
  ctx.form.vehicle.vin.disable();
  ctx.form.vehicle.vin.setValue('');
}); // NO OPTIONS - BAD!
watchField(path.insuranceType, (_, ctx) => {
  ctx.form.property.type.disable(); // CYCLE!
}); // NO OPTIONS - BAD!

// CORRECT - consolidate into ONE watcher with guards AND { immediate: false }
watchField(
  path.insuranceType,
  (_, ctx) => {
    const type = ctx.form.insuranceType.value.value;
    const isVehicle = type === 'casco';

    // Guard: only disable if not already disabled
    if (!isVehicle && !ctx.form.vehicle.vin.disabled.value) {
      ctx.form.vehicle.vin.disable();
    }
    // Guard: only setValue if value differs
    if (!isVehicle && ctx.form.vehicle.vin.getValue() !== '') {
      ctx.form.vehicle.vin.setValue('');
    }
    // Arrays: compare by length, not reference
    if (!isVehicle) {
      const drivers = ctx.form.drivers.getValue();
      if (Array.isArray(drivers) && drivers.length > 0) {
        ctx.form.drivers.setValue([]);
      }
    }
  },
  { immediate: false }
); // REQUIRED!

// BEST - use enableWhen instead of watchField
enableWhen(path.vehicle.vin, (form) => form.insuranceType === 'casco', { resetOnDisable: true });
```

See `22-cycle-detection.md` for complete pattern.

### Cross-field валидация — через `validate` + `root`

```typescript
// Validator signature: (value, control, root) => ValidationError | null
// Cross-field правило вешается на ПОЛЕ-НОСИТЕЛЬ ошибки. Соседние поля читаются через `root`.

// CORRECT — ошибка ложится на path.field1
validate(path.field1, (_value, _control, root) => {
  const form = root.getValue();         // typed as TForm
  if (form.field1 > form.field2) {
    return { code: 'error', message: 'Invalid' };
  }
  return null;
});

// CORRECT — те же поля через reactive-доступ
validate(path.address.city, (value, _control, root) => {
  if (!value && root.address.region.value.value) {
    return { code: 'cityRequired', message: 'City required when region is set' };
  }
  return null;
});

// WRONG - old API removed
validateGroup(scopePath, (scope, root) => { ... });  // validateGroup убран
validateTree((ctx) => { ... });                       // validateTree no longer exists
```

## 16. 14. PROJECT STRUCTURE (COLOCATION)

```
src/
├── components/ui/                    # Reusable UI components
│   ├── FormField.tsx
│   └── FormArrayManager.tsx
│
├── forms/
│   └── [form-name]/                  # Form module
│       ├── type.ts                   # Main form type
│       ├── schema.ts                 # Main schema
│       ├── validators.ts             # Validators
│       ├── behaviors.ts              # Behaviors
│       ├── [FormName]Form.tsx        # Main component
│       │
│       ├── steps/                    # Multi-step wizard
│       │   ├── loan-info/
│       │   │   ├── type.ts
│       │   │   ├── schema.ts
│       │   │   ├── validators.ts
│       │   │   ├── behaviors.ts
│       │   │   └── LoanInfoForm.tsx
│       │   └── ...
│       │
│       └── sub-forms/                # Reusable sub-forms
│           ├── address/
│           └── personal-data/
```

### Key Files

```typescript
// forms/credit-application/type.ts
export type { LoanInfoStep } from './steps/loan-info/type';
export interface CreditApplicationForm {
  loanType: LoanType;
  loanAmount: number;
  // ...
}

// forms/credit-application/schema.ts
import { loanInfoSchema } from './steps/loan-info/schema';
export const creditApplicationSchema = {
  ...loanInfoSchema,
  monthlyPayment: { value: 0, disabled: true },
};

// forms/credit-application/validators.ts
import { loanValidation } from './steps/loan-info/validators';
export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
  loanValidation(path);
  // Cross-step validation...
};
```

### Scaling

| Complexity | Structure                                                           |
| ---------- | ------------------------------------------------------------------- |
| Simple     | Single file: `ContactForm.tsx`                                      |
| Medium     | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
| Complex    | Full colocation with `steps/` and `sub-forms/`                      |

## 17. 14.5 UI COMPONENT PATTERNS

> **Default rule (read first)**: для UI используй `FormField` из
> [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
> строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если
> ui-kit не подходит (другая design system, особый low-level input).
>
> Канонический schema-driven подход:
>
> - **компонент** объявляется в схеме как `component: Input` (или `Select`, `Checkbox`, etc.)
> - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
> - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
>
> См. `find_recipe(package="@reformer/ui-kit", topic="form-field-integration")`
> для полного руководства.

### Default — FormField из ui-kit (canonical)

```tsx
import { useMemo } from 'react';
import { createForm, type FormSchema } from '@reformer/core';
import { FormField, Input, Select, Checkbox, Button } from '@reformer/ui-kit';

type RegistrationForm = {
  email: string;
  country: string;
  agree: boolean;
};

function RegistrationPage() {
  const form = useMemo(
    () =>
      createForm<RegistrationForm>({
        form: {
          email: {
            value: '',
            component: Input,
            componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
          },
          country: {
            value: 'ru',
            component: Select,
            componentProps: {
              label: 'Country',
              options: [
                { value: 'ru', label: 'Россия' },
                { value: 'by', label: 'Беларусь' },
              ],
            },
          },
          agree: {
            value: false,
            component: Checkbox,
            componentProps: { label: 'I agree to terms' },
          },
        } satisfies FormSchema<RegistrationForm>,
      }),
    []
  );

  return (
    <form>
      <FormField control={form.email} testId="email" />
      <FormField control={form.country} testId="country" />
      <FormField control={form.agree} testId="agree" />
      <Button type="submit">Register</Button>
    </form>
  );
}
```

`FormField` сам читает `componentProps.label`, `componentProps.placeholder`,
`componentProps.options` через `useFormControl(...).componentProps` и применяет
их к нужному `<input>`/`<select>`/etc. Error rendering, `pending` для async-валидаций,
`data-testid` для e2e — всё из коробки.

### Anti-patterns (не делай так)

❌ **Свои field-компоненты с label-prop'ами в JSX**:

```tsx
// WRONG — дублирует логику FormField, ломает schema-driven архитектуру
<Input control={form.email} label="Email" placeholder="..." />
<Select control={form.country} options={[...]} />
```

❌ **Передача компонент-пропсов через JSX вместо схемы**:

```tsx
// WRONG — нарушает single source of truth (схема)
<FormField control={form.email} label="Email" />
```

✅ Всё это в схеме:

```ts
{ email: { component: Input, componentProps: { label: 'Email' } } }
```

```tsx
<FormField control={form.email} />
```

### Advanced — кастомный input через `children` slot

Когда нужен низкоуровневый input, которого нет в ui-kit (маска, особый combobox):

```tsx
import { FormField } from '@reformer/ui-kit';
import { InputMask } from 'react-input-mask';

<FormField control={form.phone} testId="phone">
  <InputMask mask="+7 (999) 999-99-99" />
</FormField>;
```

`children` оборачивается в `CdkFormField.Control asChild` и получает все нужные
props (`value`, `onChange`, `onBlur`, `aria-invalid`).

### Advanced — write your own from scratch (rare)

Если ты не хочешь подключать `@reformer/ui-kit`, пиши свои компоненты на основе
`useFormControl` — но **сохраняй schema-driven подход**: читай label/placeholder
из `componentProps`, не из JSX-props.

```tsx
import type { FieldNode } from '@reformer/core';
import { useFormControl } from '@reformer/core';

type MyFormFieldProps<T> = { control: FieldNode<T> }; // ← ОДИН prop

function MyFormField<T>({ control }: MyFormFieldProps<T>) {
  const { value, errors, disabled, shouldShowError, componentProps } = useFormControl(control);
  // componentProps = { label, placeholder, type, options, ... } — из СХЕМЫ
  const cp = (componentProps ?? {}) as Record<string, unknown>;

  return (
    <label>
      {cp.label && <span>{cp.label as string}</span>}
      <input
        type={(cp.type as string) ?? 'text'}
        value={(value ?? '') as string}
        placeholder={cp.placeholder as string | undefined}
        disabled={disabled}
        onChange={(e) => (control.setValue as (v: unknown) => void)(e.target.value)}
        onBlur={() => control.markAsTouched()}
      />
      {shouldShowError && errors[0] && <span>{errors[0].message}</span>}
    </label>
  );
}
```

Использование — как у `FormField`:

```tsx
<MyFormField control={form.email} /> // ← без label-prop
```

### Integration with UI libraries (shadcn etc.)

Если есть существующая design system — оборачивай её компоненты в один
`MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
тип input'а — пусть `componentProps.type` диспатчит внутри.

```tsx
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

function ShadcnFormField({ control }: { control: FieldNode<string> }) {
  const { value, errors, disabled, componentProps } = useFormControl(control);
  const cp = (componentProps ?? {}) as Record<string, unknown>;

  return (
    <div className="space-y-2">
      {cp.label && <Label>{cp.label as string}</Label>}
      <Input
        value={(value ?? '') as string}
        onChange={(e) => control.setValue(e.target.value)}
        disabled={disabled}
      />
      {errors[0] && <p className="text-red-500">{errors[0].message}</p>}
    </div>
  );
}
```

## 18. 15. NON-EXISTENT API (DO NOT USE)

**The following APIs do NOT exist in @reformer/core:**

| Wrong               | Correct                          | Notes                            |
| ------------------- | -------------------------------- | -------------------------------- |
| `useForm`           | `createForm`                     | There is no useForm hook         |
| `FieldSchema`       | `FieldConfig<T>`                 | Type for individual field config |
| `when()`            | `applyWhen()`                    | Conditional validation function  |
| `FormFields`        | `FieldNode<T>`                   | Type for field nodes             |
| `FormInstance<T>`   | `FormProxy<T>`                   | Form type for component props    |
| `useArrayField()`   | `form.items.push/map/removeAt`   | Use ArrayNode methods directly   |
| `FormProvider`      | `<Component form={form} />`      | Pass form via props, no context  |
| `formState`         | `form.valid`, `form.dirty`, etc. | Separate signals on form         |
| `control` prop      | Not needed                       | Form IS the control              |
| `register('field')` | `useFormControl(form.field)`     | Type-safe field access           |
| `getFieldValue()`   | `ctx.form.field.value.value`     | Read via signals                 |

### Common Import Errors

```typescript
// WRONG - These do NOT exist
import { useForm } from '@reformer/core'; // NO!
import { when } from '@reformer/core/validators'; // NO!
import type { FieldSchema } from '@reformer/core'; // NO!
import type { FormFields } from '@reformer/core'; // NO!

// CORRECT
import { createForm, useFormControl } from '@reformer/core';
import { applyWhen } from '@reformer/core/validators';
import type { FieldConfig, FieldNode } from '@reformer/core';
```

### FormSchema Common Mistakes

```typescript
// WRONG - Simple values don't work
const schema = {
  name: '', // Missing { value, component }
  email: '', // Missing { value, component }
};

// CORRECT - Every field needs value and component
const schema: FormSchema<MyForm> = {
  name: {
    value: '',
    component: Input,
    componentProps: { label: 'Name' },
  },
  email: {
    value: '',
    component: Input,
    componentProps: { label: 'Email', type: 'email' },
  },
};
```

## 19. 16. READING FIELD VALUES (CRITICALLY IMPORTANT)

### Why .value.value?

ReFormer uses `@preact/signals-core` for reactivity:

- `field.value` -> `Signal<T>` (reactive container)
- `field.value.value` -> `T` (actual value)
- `field.getValue()` -> `T` (shorthand method, non-reactive)

```typescript
// Reading values in different contexts:

// In React components - use hooks
const { value } = useFormControl(control.email); // Object with value
const email = useFormControlValue(control.email); // Value directly

// In BehaviorContext (watchField, etc.)
watchField(
  path.firstName,
  (firstName, ctx) => {
    // ctx.form is typed as the PARENT GROUP of the watched field!
    // For path.nested.field: ctx.form = NestedType, NOT RootForm!

    const lastName = ctx.form.lastName.value.value; // Read sibling field

    // Use setFieldValue with full path for root-level fields
    ctx.setFieldValue('fullName', `${firstName} ${lastName}`);
  },
  { immediate: false }
); // REQUIRED to prevent cycle detection!

// Direct access on form controls
form.email.value.value; // Read current value
form.address.city.value.value; // Read nested value
```

### Reading Nested Values in watchField

```typescript
// IMPORTANT: ctx.form type depends on the watched path!

// Watching root-level field
watchField(
  path.loanAmount,
  (amount, ctx) => {
    // ctx.form is MyForm - can access all fields
    const rate = ctx.form.interestRate.value.value;
    ctx.setFieldValue('monthlyPayment', (amount * rate) / 12);
  },
  { immediate: false }
);

// Watching nested field
watchField(
  path.personalData.lastName,
  (lastName, ctx) => {
    // ctx.form is PersonalData, NOT MyForm!
    const firstName = ctx.form.firstName.value.value; // Works
    const middleName = ctx.form.middleName.value.value; // Works

    // For root-level field, use setFieldValue with full path
    ctx.setFieldValue('fullName', `${lastName} ${firstName}`);
  },
  { immediate: false }
);
```

## 20. 17. COMPUTE FROM vs WATCH FIELD

### computeFrom — Same Nesting Level (annotate destructured arg!)

The compute callback receives the **whole form** (`(values: TForm) => T`),
so to get exact field types on the destructured argument you must annotate
it with the form type. Without annotation, fields are typed as `unknown`
and you'll be forced into `as` casts.

```typescript
// ✅ Works: all source fields at the same level, annotated with form type
computeFrom(
  [path.price, path.quantity],
  path.total,
  ({ price, quantity }: MyForm) => (price ?? 0) * (quantity ?? 0)
);

// ✅ Works: nested-to-nested at same level
computeFrom(
  [path.address.houseNumber, path.address.streetName],
  path.address.fullAddress,
  ({ address }: MyForm) => `${address.houseNumber} ${address.streetName}`
);

// ✅ Cross-level via group-node subscription: subscribe to the group node,
//    not individual leaves; destructure the group inside.
computeFrom([path.personalData], path.fullName, ({ personalData }: MyForm) =>
  [personalData.firstName, personalData.lastName].filter(Boolean).join(' ')
);

// ❌ FAILS: subscribing nested leaves with target at root level
computeFrom(
  [path.nested.price, path.nested.quantity],
  path.rootTotal,
  ...
);
// Use group-node subscription (above) or watchField (below) instead.
```

### watchField - Any Level

```typescript
// Works for cross-level computation
watchField(
  path.nested.price,
  (price, ctx) => {
    const quantity = ctx.form.quantity.value.value; // Sibling in nested
    ctx.form.rootTotal.setValue(price * quantity); // ctx.form.<path>.setValue, NOT ctx.setFieldValue
  },
  { immediate: false }
); // REQUIRED!

// Works for multiple dependencies
watchField(
  path.loanAmount,
  (amount, ctx) => {
    const term = ctx.form.loanTerm.value.value;
    const rate = ctx.form.interestRate.value.value;

    if (amount && term && rate) {
      const monthly = calculateMonthlyPayment(amount, term, rate);
      // Guard: only setValue if value really changed (cycle prevention)
      if (Math.abs(ctx.form.monthlyPayment.value.value - monthly) > 0.01) {
        ctx.form.monthlyPayment.setValue(monthly);
      }
    }
  },
  { immediate: false }
); // REQUIRED!
```

### Cross-level write API

Inside a `watchField` callback, write to other fields via `ctx.form.<path>.setValue(value)` —
**`ctx.setFieldValue(name, value)` does not exist** (that was a documentation typo, fixed).
For reads use `ctx.form.<path>.value.value` (the inner `.value` reads the signal).
For enable/disable: `ctx.form.<path>.enable()` / `.disable()` / read `.disabled.value`.

````

### Rule of Thumb

| Scenario | Use |
|----------|-----|
| All fields share same parent | `computeFrom` (simpler, auto-cleanup) |
| Fields at different levels | `watchField` (more flexible) |
| Multiple dependencies | `watchField` |
| Async computation | `watchField` with async callback |

### Stage-pattern for chained computeds

When several computeds depend on each other (e.g. `interestRate` → `monthlyPayment` → `paymentToIncomeRatio`), put the whole chain inside ONE `watchField` for the upstream trigger. Do NOT split into one watcher per target — that creates cross-watcher signal bouncing and risks cycles. Read intermediate values from the `new` local you just computed, not from `ctx.form.intermediate.value.value` (which still holds the old value during the cascade).

### Multiple triggers, one cascade

`watchField` accepts a **single** `FieldPathNode` — there is no `watchField([pathA, pathB], ...)` overload. If a single computed depends on several independent triggers (e.g. `monthlyPayment` depends on `loanAmount`, `loanTerm`, `interestRate`), register one `watchField` per trigger and have them all call the same compute function:

```typescript
function recomputeMonthlyPayment(ctx: BehaviorContext<MyForm>) {
  const amount = ctx.form.step1.loanAmount.value.value;
  const term   = ctx.form.step1.loanTerm.value.value;
  const rate   = ctx.form.interestRate.value.value;
  if (!amount || !term || !rate) return;
  const monthly = annuity(amount, term, rate);
  if (Math.abs((ctx.form.monthlyPayment.value.value as number) - monthly) > 0.01) {
    ctx.form.monthlyPayment.setValue(monthly);
  }
}

watchField(path.step1.loanAmount, (_v, ctx) => recomputeMonthlyPayment(ctx), { immediate: false });
watchField(path.step1.loanTerm,   (_v, ctx) => recomputeMonthlyPayment(ctx), { immediate: false });
watchField(path.interestRate,     (_v, ctx) => recomputeMonthlyPayment(ctx), { immediate: false });
````

The "one watcher per trigger" cycle-prevention rule means **never register two watchField on the same path**, not "consolidate all triggers into one". Multiple watchers on different trigger paths is the canonical pattern for multi-source recomputes — try `watchField([…], …)` and you get a runtime `getFieldByPath` failure (the array form does not exist).

## 21. 18. ARRAY OPERATIONS

### Array Access - CRITICAL

```typescript
// WRONG - bracket notation does NOT work!
const first = form.items[0];        // undefined or error
const second = form.items[1];       // undefined or error

// CORRECT - use .at() method
const first = form.items.at(0);     // FormProxy<ItemType> | undefined
const second = form.items.at(1);    // FormProxy<ItemType> | undefined

// CORRECT - iterate with map (most common pattern)
form.items.map((item, index) => {
  // item is fully typed GroupNode
  item.name.setValue('New Name');
  item.price.value.value;  // read value
});
```

### Array Methods

```typescript
// Add items
form.items.push({ name: '', price: 0 });           // Add to end
form.items.insert(0, { name: '', price: 0 });      // Insert at index

// Remove items
form.items.removeAt(index);                         // Remove by index
form.items.clear();                                 // Remove all items

// Reorder
form.items.move(fromIndex, toIndex);                // Move item

// Access (use .at(), NOT brackets!)
form.items.length.value;                            // Current length (Signal)
form.items.map((item, index) => ...);               // Iterate items
form.items.at(index);                               // Get item at index (NOT items[index]!)
```

### Rendering Arrays

```tsx
function ItemsList({ form }: { form: FormProxy<MyForm> }) {
  const { length } = useFormControl(form.items);

  return (
    <div>
      {form.items.map((item, index) => (
        // item is GroupNode (sub-form) - each field is a control
        <div key={item.id || index}>
          <FormField control={item.name} />
          <FormField control={item.price} />
          <button onClick={() => form.items.removeAt(index)}>Remove</button>
        </div>
      ))}

      {length === 0 && <p>No items yet</p>}

      <button onClick={() => form.items.push({ name: '', price: 0 })}>
        Add Item
      </button>
    </div>
  );
}
```

### Array Cross-Validation

Cross-field правила по массиву пишутся как обычный `Validator`, вешаются на сам массив
(или на любое поле-носитель ошибки), а данные читаются через `root`.

```typescript
// Validate uniqueness across array items
validate(path.items, (_value, _control, root) => {
  const items = root.items;
  const names = items.map((item) => item.name.value.value);
  const uniqueNames = new Set(names);

  if (names.length !== uniqueNames.size) {
    return { code: 'duplicate', message: 'Item names must be unique' };
  }
  return null;
});

// Validate sum of percentages
validate(path.items, (_value, _control, root) => {
  const items = root.items;
  const totalPercent = items.reduce(
    (sum, item) => sum + (item.percentage.value.value || 0),
    0
  );

  if (Math.abs(totalPercent - 100) > 0.01) {
    return { code: 'invalid_total', message: 'Percentages must sum to 100%' };
  }
  return null;
});
```

## 22. Cycle Detection Prevention Checklist

**ALWAYS follow these rules to prevent "Cycle detected" error:**

1. ✅ **ONE watchField per trigger field** - consolidate all logic into single handler
2. ✅ **ALWAYS use `{ immediate: false }`** - required option for watchField
3. ✅ **Guard all disable/enable calls** - check `field.disabled.value` before calling
4. ✅ **Guard all setValue calls** - only call if value actually differs
5. ✅ **Arrays: compare by length** - `[] !== []` is always true, use `.length`
6. ✅ **Prefer enableWhen over watchField** - for simple enable/disable logic

---

## 23. Cycle Detected Error

### Problem

Error `Cycle detected` occurs when reactive system detects circular dependency during field updates.

### Root Cause

Multiple `watchField` handlers on the same field (e.g., `path.insuranceType`) each calling `disable()` and `setValue()` creates reactive cycles:

```typescript
// WRONG - Multiple watchers on same field + missing { immediate: false }
watchField(path.insuranceType, (_, ctx) => {
  // Handler 1: vehicle fields
  if (!isVehicle) {
    ctx.form.vehicle.vin.disable();
    ctx.form.vehicle.vin.setValue('');
  }
}); // NO OPTIONS - BAD!

watchField(path.insuranceType, (_, ctx) => {
  // Handler 2: property fields - CAUSES CYCLE!
  if (!isProperty) {
    ctx.form.property.type.disable();
    ctx.form.property.type.setValue('');
  }
}); // NO OPTIONS - BAD!

// More watchers on same field = more cycles
```

### Solution

1. **Consolidate all watchers for same field into ONE handler**
2. **Check state before calling disable/enable/setValue**
3. **ALWAYS add `{ immediate: false }` option**

```typescript
// CORRECT - Single consolidated watcher with guards AND { immediate: false }
watchField(
  path.insuranceType,
  (_value, ctx) => {
    const insuranceType = ctx.form.insuranceType.value.value;
    const isVehicle = insuranceType === 'casco' || insuranceType === 'osago';
    const isProperty = insuranceType === 'property';

    // Helper: check if array value needs update (compare by length, not reference)
    const needsValueUpdate = <T>(current: T, defaultVal: T): boolean => {
      if (Array.isArray(current) && Array.isArray(defaultVal)) {
        return current.length !== defaultVal.length;
      }
      return current !== defaultVal;
    };

    // Helper: disable only if not already disabled, setValue only if different
    const disableAndReset = <T>(
      field:
        | {
            disable: () => void;
            setValue: (v: T) => void;
            getValue: () => T;
            disabled: { value: boolean };
          }
        | undefined,
      defaultValue: T
    ) => {
      if (field) {
        if (!field.disabled.value) {
          field.disable();
        }
        if (needsValueUpdate(field.getValue(), defaultValue)) {
          field.setValue(defaultValue);
        }
      }
    };

    const enableField = (
      field: { enable: () => void; disabled: { value: boolean } } | undefined
    ) => {
      if (field && field.disabled.value) {
        field.enable();
      }
    };

    // --- All vehicle fields in one place ---
    if (isVehicle) {
      enableField(ctx.form.vehicle.vin);
      enableField(ctx.form.vehicle.brand);
    } else {
      disableAndReset(ctx.form.vehicle.vin, '');
      disableAndReset(ctx.form.vehicle.brand, '');
    }

    // --- All property fields in one place ---
    if (isProperty) {
      enableField(ctx.form.property.type);
    } else {
      disableAndReset(ctx.form.property.type, '');
    }

    // --- Arrays: compare by length ---
    if (isVehicle) {
      enableField(ctx.form.drivers);
    } else {
      disableAndReset(ctx.form.drivers, []); // Won't call setValue if already empty
    }
  },
  { immediate: false }
); // REQUIRED!
```

### Prefer Built-in Behaviors

**Instead of complex watchField with guards, use built-in behaviors when possible:**

```typescript
// ❌ COMPLEX - watchField with manual guards (error-prone)
watchField(
  path.insuranceType,
  (_value, ctx) => {
    const isVehicle = ctx.form.insuranceType.value.value === 'casco';
    if (isVehicle) {
      if (ctx.form.vehicle.vin.disabled.value) ctx.form.vehicle.vin.enable();
    } else {
      if (!ctx.form.vehicle.vin.disabled.value) ctx.form.vehicle.vin.disable();
      if (ctx.form.vehicle.vin.getValue() !== '') ctx.form.vehicle.vin.setValue('');
    }
  },
  { immediate: false }
);

// ✅ SIMPLE - enableWhen with resetOnDisable (recommended)
enableWhen(path.vehicle.vin, (form) => form.insuranceType === 'casco', { resetOnDisable: true });
enableWhen(path.vehicle.brand, (form) => form.insuranceType === 'casco', { resetOnDisable: true });
```

### Key Rules

1. **ONE watcher per trigger field** - consolidate all logic for `insuranceType` into single `watchField`
2. **ALWAYS use `{ immediate: false }`** - prevents execution during initialization
3. **Guard disable()** - only call if `!field.disabled.value`
4. **Guard enable()** - only call if `field.disabled.value`
5. **Guard setValue()** - only call if value actually differs
6. **Arrays special case** - compare by `.length`, not by reference (`[] !== []` is always true)

### Other Watchers

For watchers on different fields (e.g., `path.health.isSmoker`), apply same guards:

```typescript
watchField(
  path.health.isSmoker,
  (_value, ctx) => {
    const isSmoker = ctx.form.health.isSmoker.value.value;
    const smokingYearsField = ctx.form.health.smokingYears;

    if (smokingYearsField) {
      if (isSmoker) {
        if (smokingYearsField.disabled.value) {
          smokingYearsField.enable();
        }
      } else {
        if (!smokingYearsField.disabled.value) {
          smokingYearsField.disable();
        }
        if (smokingYearsField.getValue() !== null) {
          smokingYearsField.setValue(null);
        }
      }
    }
  },
  { immediate: false }
); // REQUIRED!
```

## 24. Purpose

`copyFrom` декларативно копирует значение одного поля (или группы) в другое при выполнении условия `when`. Используется для UX-сценариев «совпадает с …»: «адрес проживания совпадает с адресом регистрации», «email для уведомлений = основной email», «billing = shipping». Альтернатива ручному `watchField` + `setValue` с управлением циклами — `copyFrom` сам выходит из reactive-контекста через `runOutsideEffect`.

## 25. API

```typescript
function copyFrom<TForm, TSource, TTarget>(
  source: FieldPathNode<TForm, TSource>,
  target: FieldPathNode<TForm, TTarget>,
  options?: CopyFromOptions<TSource, TForm>
): void;

interface CopyFromOptions<TSource, TForm = unknown> {
  /** Условие копирования. Если не задано — копирует всегда при изменении source. */
  when?: (form: TForm) => boolean;

  /** Какие поля копировать для группы. По умолчанию 'all'. */
  fields?: (keyof TSource)[] | 'all';

  /** Преобразование значения перед записью в target. */
  transform?: (value: TSource) => unknown;

  /** Debounce срабатывания в миллисекундах. */
  debounce?: number;
}
```

Вызывается строго внутри `BehaviorSchemaFn` (поведенческой схемы формы). Возвращает `void`; cleanup управляется реестром поведения формы.

## 26. Examples

### Базовый сценарий — синхронизация двух адресов целиком

```typescript
import { copyFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface OrderForm {
  useShippingAsBilling: boolean;
  shippingAddress: string;
  billingAddress: string;
}

export const orderBehavior: BehaviorSchemaFn<OrderForm> = (path) => {
  copyFrom(path.shippingAddress, path.billingAddress, {
    when: (form) => form.useShippingAsBilling === true,
  });
};
```

Source: `BehaviorsExamples.tsx:227` (monorepo example).

### Копирование группы — только выбранные подполя

```typescript
import { copyFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface Address {
  country: string;
  region: string;
  city: string;
  street: string;
  postalCode: string;
}

interface ProfileForm {
  sameAsRegistration: boolean;
  registrationAddress: Address;
  residenceAddress: Address;
}

export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
  // Копируем только country/region/city, локальные значения street сохраняем
  copyFrom(path.registrationAddress, path.residenceAddress, {
    when: (form) => form.sameAsRegistration === true,
    fields: ['country', 'region', 'city'],
  });
};
```

### Совмещение с `apply([...])` — переиспользование между несколькими полями

```typescript
import { copyFrom, apply, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface AddressBlock {
  source: string;
  copy: string;
  copyEnabled: boolean;
}

const addressCopyBehavior: BehaviorSchemaFn<AddressBlock> = (path) => {
  copyFrom(path.source, path.copy, {
    when: (form) => form.copyEnabled === true,
  });
};

interface ProfileForm {
  homeAddress: AddressBlock;
  workAddress: AddressBlock;
}

export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
  // Применяем одну схему копирования к двум вложенным группам
  apply([path.homeAddress, path.workAddress], addressCopyBehavior);
};
```

Source: `credit-application-behavior.ts:69-79` (monorepo example).

### Copy + transform — нормализация при копировании

```typescript
import { copyFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface ContactForm {
  sameEmail: boolean;
  email: string;
  emailAdditional: string;
}

export const contactBehavior: BehaviorSchemaFn<ContactForm> = (path) => {
  copyFrom(path.email, path.emailAdditional, {
    when: (form) => form.sameEmail === true,
    transform: (value) => (typeof value === 'string' ? value.trim().toLowerCase() : value),
    debounce: 200,
  });
};
```

## 27. Anti-patterns

```typescript
// ❌ Ручной watchField с setValue — пишите copyFrom
watchField(path.shippingAddress, (value, ctx) => {
  if (ctx.form.useShippingAsBilling.value.value) {
    ctx.form.billingAddress.setValue(value); // приведёт к "Cycle detected" без runOutsideEffect
  }
});

// ✅ copyFrom уже использует runOutsideEffect внутри
copyFrom(path.shippingAddress, path.billingAddress, {
  when: (form) => form.useShippingAsBilling === true,
});
```

```typescript
// ❌ Не пытайтесь вернуть данные обратно — copyFrom однонаправленный
copyFrom(path.a, path.b);
copyFrom(path.b, path.a); // ЦИКЛ! Используйте syncFields для двусторонней связи

// ✅ Двусторонняя синхронизация — это работа syncFields
syncFields(path.a, path.b);
```

```typescript
// ❌ when, который читает само target-поле
copyFrom(path.source, path.target, {
  when: (form) => form.target === '', // лишний triger при перезаписи target
});

// ✅ when опирается только на флаг/независимое поле
copyFrom(path.source, path.target, {
  when: (form) => form.copyEnabled === true,
});
```

```typescript
// ❌ Передача fields для не-групповых полей
copyFrom(path.email, path.emailCopy, { fields: ['something'] }); // fields игнорируется для скаляров

// ✅ Для скаляров — без fields, или fields: 'all'
copyFrom(path.email, path.emailCopy);
```

## 28. Troubleshooting

**Q: Скопированное значение не появляется в target.**
A: Проверьте, что (1) вы внутри `BehaviorSchemaFn`, переданной в `createForm({ behavior })`; (2) `when` возвращает `true` (поставьте `console.log` в `when`); (3) тип source/target совместим — `copyFrom` записывает значение «как есть», без неявных конверсий.

**Q: «Cycle detected» при копировании.**
A: Это происходит, если `when` использует значение target-поля (см. anti-pattern выше) или вы дополнительно навесили `watchField`/`computeFrom` на target. Изолируйте триггеры: condition должно зависеть только от source и от независимых флагов.

**Q: Целевая группа теряет несколько полей.**
A: По умолчанию `fields: 'all'` копирует **всё значение** через `setValue`, перетирая поля, которых нет в source. Если нужно сохранить часть target — укажите `fields: ['onlyThese']`, тогда применится `patchValue` (мердж).

**Q: copyFrom не реагирует на загрузку начальных значений.**
A: `copyFrom` подписывается на изменения source через `watchField` без `immediate`. Первичное копирование произойдёт при первом изменении source. Для синхронизации сразу после `patchValue` — вызывайте `patchValue` для target вручную в hooks загрузки (см. [29-async-preload.md](./29-async-preload.md)).

**Q: Как откатить копию, если пользователь снял флаг `when`?**
A: `copyFrom` сам по себе не сбрасывает target при `when === false` — он только не пишет. Для сброса параллельно используйте `resetWhen(path.target, (form) => !form.copyEnabled)` (см. [25-reset-when.md](./25-reset-when.md)).

## 29. See also

- [24-sync-fields.md](./24-sync-fields.md) — двусторонняя синхронизация
- [25-reset-when.md](./25-reset-when.md) — сброс target при выключенном условии
- [11-async-watchfield.md](./11-async-watchfield.md) — низкоуровневый `watchField`, на котором построен `copyFrom`
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `runOutsideEffect` нужен и как его избежать руками

## 30. Purpose

`syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них переписывает второе. Применяется для дублей одного значения в разных частях формы (тех. поле + видимое представление, мобильный/десктопный input, mirror-поля для отчётов). Внутренний флаг `isUpdating` плюс `runOutsideEffect` исключают петли. Если нужно одностороннее копирование — берите [`copyFrom`](./23-copy-from.md), для расчётов — [`computeFrom`](./03-api-signatures.md).

## 31. API

```typescript
function syncFields<TForm extends FormFields, T extends FormValue>(
  field1: FieldPathNode<TForm, T>,
  field2: FieldPathNode<TForm, T>,
  options?: SyncFieldsOptions<T>
): void;

interface SyncFieldsOptions<T> {
  /** Преобразование при синхронизации field1 → field2 (НЕ применяется в обратную сторону). */
  transform?: (value: T) => T;

  /** Debounce в миллисекундах. */
  debounce?: number;
}
```

Поля должны иметь совместимый тип `T`. `transform` асимметричен: он работает только при движении значения от `field1` к `field2`.

## 32. Examples

### Базовый сценарий — отзеркаливание текста

```typescript
import { syncFields, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface MirrorForm {
  syncField1: string;
  syncField2: string;
}

export const mirrorBehavior: BehaviorSchemaFn<MirrorForm> = (path) => {
  syncFields(path.syncField1, path.syncField2);
};
```

Source: `BehaviorsExamples.tsx:249` (monorepo example).

### С трансформацией — нормализация при прямой записи

```typescript
import { syncFields, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface DisplayForm {
  internalCode: string; // канонический формат
  displayCode: string; // показываем пользователю
}

export const codeBehavior: BehaviorSchemaFn<DisplayForm> = (path) => {
  // internalCode → displayCode: нормализация в верхний регистр
  // displayCode → internalCode: значение пишется как есть
  syncFields(path.internalCode, path.displayCode, {
    transform: (value) => (typeof value === 'string' ? value.toUpperCase() : value),
    debounce: 150,
  });
};
```

### Edge case — sync с независимой валидацией

```typescript
import { syncFields, revalidateWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
import { required, pattern } from '@reformer/core/validators';

interface ContactForm {
  phoneA: string;
  phoneB: string;
}

export const contactValidation = (path: FieldPath<ContactForm>) => {
  required(path.phoneA);
  pattern(path.phoneB, /^\+\d{10,12}$/);
};

export const contactBehavior: BehaviorSchemaFn<ContactForm> = (path) => {
  syncFields(path.phoneA, path.phoneB);

  // Когда явно нужно перезапустить валидацию обоих после синхронизации
  revalidateWhen(path.phoneA, [path.phoneB]);
  revalidateWhen(path.phoneB, [path.phoneA]);
};
```

## 33. Anti-patterns

```typescript
// ❌ Симметрично через два copyFrom — приведёт к "Cycle detected"
copyFrom(path.a, path.b);
copyFrom(path.b, path.a);

// ✅ syncFields умеет двусторонней связь без циклов
syncFields(path.a, path.b);
```

```typescript
// ❌ Ожидание, что transform применится в обе стороны
syncFields(path.a, path.b, { transform: (v) => v.trim() });
// при записи в b значение НЕ trim-ается

// ✅ Если нужны симметричные трансформы, делайте syncFields + transformValue
import { transformValue } from '@reformer/core/behaviors';
syncFields(path.a, path.b);
transformValue(path.a, (v) => (typeof v === 'string' ? v.trim() : v));
transformValue(path.b, (v) => (typeof v === 'string' ? v.trim() : v));
```

```typescript
// ❌ Поля разного типа — рантайм-приведение и баги
syncFields(path.amountString, path.amountNumber); // string ↔ number

// ✅ Используйте computeFrom + ручное обратное связывание
computeFrom([path.amountString], path.amountNumber, (v) => Number(v.amountString));
```

```typescript
// ❌ Sync для FormArray/FormGroup — компонент не сравнивает по содержимому
syncFields(path.itemsA, path.itemsB); // ссылочное равенство, потенциально лишние перезаписи

// ✅ Для коллекций — copyFrom + явный fields/transform либо ручной watchField
```

## 34. Troubleshooting

**Q: Поля «дёргаются», вижу несколько перезаписей.**
A: Чаще всего внутри одного из полей висит `transformValue` или `computeFrom`. Передайте `debounce: 100…300` в `syncFields`, и/или проверьте, что у источника transform идемпотентен (`f(f(x)) === f(x)`).

**Q: Курсор/каретка прыгает в input.**
A: Симптом частых `setValue`. Поднимите `debounce` (минимум 150 ms) и убедитесь, что `transform` стабильный (не возвращает `new String(...)` или другой объект-обёртку). Лучше — храните «канонический» формат в одном поле и считайте отображаемый через `computeFrom`.

**Q: Sync не работает после `form.reset()`.**
A: `reset()` устанавливает оба поля одновременно. Это нормально — sync догонится при первом следующем изменении. Если нужна согласованность сразу после reset — задайте одинаковые initial values в схеме.

**Q: «Cycle detected» при включённом `syncFields`.**
A: Не вешайте `watchField(path.a, …)` + `watchField(path.b, …)`, которые сами пишут друг в друга. `syncFields` уже занимает оба направления. Перенесите side-эффекты в `watchField` на одно поле и не трогайте второе изнутри callback.

**Q: Как ограничить sync условием (как `when` у copyFrom)?**
A: У `syncFields` нет `when`. Сэмулируйте через `apply` под условием либо комбинируйте `copyFrom(a → b, { when })` и `copyFrom(b → a, { when })` с разными флагами, разрешая только одну активную сторону за раз.

## 35. See also

- [23-copy-from.md](./23-copy-from.md) — однонаправленное копирование с `when`
- [26-transform-value.md](./26-transform-value.md) — нормализация значений на месте
- [22-cycle-detection.md](./22-cycle-detection.md) — почему симметричный copy ломается
- [03-api-signatures.md](./03-api-signatures.md) — `computeFrom` для производных значений

## 36. Purpose

`resetWhen` сбрасывает значение поля и его `dirty/touched`-флаги при выполнении условия от формы. Это альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но содержимое нужно очистить (например, переключение типа оплаты обнуляет «номер карты», но поле всё ещё доступно для ручного ввода). По умолчанию пишет `null`; через `resetValue` задаётся произвольное значение, через `onlyIfDirty` — пропуск нетронутых пользователем полей.

## 37. API

```typescript
function resetWhen<TForm extends FormFields>(
  field: FieldPathNode<TForm, FormValue>,
  condition: (form: TForm) => boolean,
  options?: ResetWhenOptions & { debounce?: number }
): void;

interface ResetWhenOptions {
  /** Значение, в которое сбрасывается поле. По умолчанию null. */
  resetValue?: FormValue;

  /** Сбрасывать только если поле dirty (пользователь его трогал). */
  onlyIfDirty?: boolean;
}
```

После сброса `markAsPristine()` и `markAsUntouched()` вызываются автоматически.

## 38. Examples

### Базовый сценарий — сброс номера карты при смене способа оплаты

```typescript
import { resetWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface CheckoutForm {
  paymentType: 'card' | 'cash';
  cardNumber: string;
}

export const checkoutBehavior: BehaviorSchemaFn<CheckoutForm> = (path) => {
  resetWhen(path.cardNumber, (form) => form.paymentType !== 'card', {
    resetValue: '',
  });
};
```

Source: `BehaviorsExamples.tsx:243-245` (monorepo example).

### С `resetValue` для числовых полей

```typescript
import { resetWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface MortgageForm {
  loanType: 'mortgage' | 'consumer' | 'car';
  propertyValue: number;
  initialPayment: number;
}

export const mortgageBehavior: BehaviorSchemaFn<MortgageForm> = (path) => {
  // initialPayment теряет смысл без propertyValue — сбрасываем в 0
  resetWhen(path.initialPayment, (form) => !form.propertyValue, {
    resetValue: 0,
  });
};
```

### `onlyIfDirty` — не трогаем нетронутое поле

```typescript
import { resetWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface CarForm {
  loanType: 'mortgage' | 'car' | 'consumer';
  carPrice: number;
}

export const carBehavior: BehaviorSchemaFn<CarForm> = (path) => {
  // Если пользователь ещё не вводил carPrice, не сбрасываем (не сломаем default)
  resetWhen(path.carPrice, (form) => form.loanType !== 'car', {
    resetValue: null,
    onlyIfDirty: true,
  });
};
```

### С `apply([...])` — общая логика для нескольких блоков

```typescript
import { resetWhen, apply, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface AddressBlock {
  enabled: boolean;
  city: string;
  street: string;
}

const addressResetBehavior: BehaviorSchemaFn<AddressBlock> = (path) => {
  resetWhen(path.city, (form) => !form.enabled, { resetValue: '' });
  resetWhen(path.street, (form) => !form.enabled, { resetValue: '' });
};

interface ProfileForm {
  homeAddress: AddressBlock;
  workAddress: AddressBlock;
}

export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
  apply([path.homeAddress, path.workAddress], addressResetBehavior);
};
```

## 39. Anti-patterns

```typescript
// ❌ resetWhen вместо enableWhen для disable-сценария
resetWhen(path.field, (form) => !form.show);
// поле останется enabled и валидируемым, ошибка required всё равно прилетит

// ✅ Если поле должно «исчезнуть», блокируем + сбрасываем
enableWhen(path.field, (form) => form.show, { resetOnDisable: true });
```

```typescript
// ❌ Сложная логика «обновить + сбросить» через ручной watchField + setValue
watchField(path.type, (_v, ctx) => {
  ctx.form.dependent.setValue(null);
  ctx.form.dependent.markAsPristine(); // забыли markAsUntouched? валидация не очистится
});

// ✅ resetWhen делает все три действия атомарно
resetWhen(path.dependent, (form) => form.type !== 'expected', { resetValue: null });
```

```typescript
// ❌ resetValue с типом, отличным от поля
resetWhen(path.amount, (form) => form.skipPayment, { resetValue: 'none' });
// рантайм-несовпадение типов: amount: number ← string

// ✅ resetValue должен быть валидным значением для FieldNode
resetWhen(path.amount, (form) => form.skipPayment, { resetValue: 0 });
```

```typescript
// ❌ Отсутствие resetValue для строкового поля
resetWhen(path.cardNumber, (form) => form.paymentType !== 'card');
// получит null, а Input ждёт string — отрисует undefined

// ✅ Явный resetValue для строк
resetWhen(path.cardNumber, (form) => form.paymentType !== 'card', { resetValue: '' });
```

## 40. Troubleshooting

**Q: Поле сбрасывается слишком часто, по любому изменению формы.**
A: `resetWhen` подписан на `form.value.value` целиком — любое изменение формы прогоняет `condition`. Если ваш `condition` возвращает `true` стабильно, сброс происходит на каждом тике. Решение: добавьте проверку текущего значения в `condition` (`condition: (form) => form.type !== 'card' && form.cardNumber !== ''`) или поднимите `debounce`.

**Q: Сброс не срабатывает, хотя `condition` возвращает true.**
A: Опция `onlyIfDirty: true` пропустит сброс, если пользователь не трогал поле. Уберите `onlyIfDirty` или явно вызовите `field.markAsDirty()`.

**Q: После сброса валидация продолжает показывать ошибку.**
A: `resetWhen` зовёт `markAsUntouched()` — `error` остаётся, но в UI поле обычно ошибки скрыты до `touched`. Если нужна жёсткая очистка ошибки, дополнительно вызовите `field.validate()` после изменения зависимого поля или используйте `revalidateWhen`.

**Q: Реактивная цепочка resetWhen → computeFrom → resetWhen ломается.**
A: Убедитесь, что `condition` не зависит от значения **самого** поля, иначе после сброса вы попадёте в новый цикл. Сравните: `condition: (form) => form.type !== 'card'` (ок) vs `condition: (form) => form.cardNumber === ''` (плохо — самотриггер).

**Q: Сбросить вложенную группу целиком — `resetValue: {}` не работает.**
A: Для group-полей лучше использовать `field.reset()` напрямую через `watchField`, либо комбинацию `enableWhen({ resetOnDisable: true })`, которая правильно проходит по поддереву. `resetWhen` рассчитан на скалярные поля.

## 41. See also

- [04-common-patterns.md](./04-common-patterns.md) — `enableWhen({ resetOnDisable: true })` как альтернатива
- [23-copy-from.md](./23-copy-from.md) — копирование, у которого нет встроенного отката
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `condition` не должен читать целевое поле
- [11-async-watchfield.md](./11-async-watchfield.md) — низкоуровневый аналог через `watchField` + `setValue`

## 42. Purpose

`transformValue` подписывается на изменение поля и переписывает его трансформированной версией: uppercase для кодов, trim+toLowerCase для email, маска для телефона, округление для чисел. В отличие от `valueParser` в `<input>`, работает декларативно в схеме формы и применяется единообразно ко всем источникам изменения (пользователь, programmatic `setValue`, `patchValue`, async `copyFrom`). Идемпотентность (`f(f(x)) === f(x)`) обязательна — иначе бесконечный цикл `setValue → callback → setValue`.

## 43. API

```typescript
function transformValue<TForm extends FormFields, TValue extends FormValue = FormValue>(
  field: FieldPathNode<TForm, TValue>,
  transformer: (value: TValue) => TValue,
  options?: TransformValueOptions & { debounce?: number },
): void;

interface TransformValueOptions {
  /** Применять только когда поле touched (т.е. правил пользователь, не programmatic). */
  onUserChangeOnly?: boolean;

  /** Эмитить событие изменения после трансформации. По умолчанию true. */
  emitEvent?: boolean;
}

// Хелперы
function createTransformer<TValue>(
  transformer: (value: TValue) => TValue,
  defaultOptions?: TransformValueOptions,
): (field: FieldPathNode<TForm, TValue>, options?: …) => void;

const transformers: {
  toUpperCase: …; toLowerCase: …; trim: …; removeSpaces: …;
  digitsOnly: …; round: …; roundTo2: …;
};
```

`setValue` вызывается только если `transformer(value) !== value` — это базовый guard от циклов.

## 44. Examples

### Базовый сценарий — uppercase для кода

```typescript
import { transformValue, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface PromoForm {
  uppercaseField: string;
}

export const promoBehavior: BehaviorSchemaFn<PromoForm> = (path) => {
  transformValue(path.uppercaseField, (value) => (value ?? '').toUpperCase());
};
```

Source: `BehaviorsExamples.tsx:239` (monorepo example).

### Несколько трансформаций — нормализация email + форматирование телефона

```typescript
import { transformValue, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface ContactForm {
  email: string;
  phone: string;
  amount: number;
}

export const contactBehavior: BehaviorSchemaFn<ContactForm> = (path) => {
  // email: trim + lowercase
  transformValue(path.email, (value) => (value ?? '').trim().toLowerCase());

  // телефон: оставить только цифры и собрать формат
  transformValue(path.phone, (value) => {
    if (!value) return value;
    const digits = value.replace(/\D/g, '');
    if (digits.length === 11) {
      return `+7 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7, 9)}-${digits.slice(9)}`;
    }
    return value;
  });

  // округление до целого
  transformValue(path.amount, (value) => (typeof value === 'number' ? Math.round(value) : value));
};
```

### С `transformers` — готовые трансформеры

```typescript
import { transformers, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface RegistrationForm {
  username: string;
  promoCode: string;
  inn: string;
  amount: number;
}

export const registrationBehavior: BehaviorSchemaFn<RegistrationForm> = (path) => {
  transformers.trim(path.username);
  transformers.toUpperCase(path.promoCode);
  transformers.digitsOnly(path.inn);
  transformers.roundTo2(path.amount);
};
```

### `onUserChangeOnly` — пропуск programmatic изменений

```typescript
import { transformValue, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface UserForm {
  preformatted: string; // изначально приходит с сервера в нужном виде
}

export const userBehavior: BehaviorSchemaFn<UserForm> = (path) => {
  // Не трогаем значение, пришедшее через patchValue от useLoadCreditApplication;
  // переформатируем только если пользователь начал править
  transformValue(path.preformatted, (value) => (value ?? '').toUpperCase(), {
    onUserChangeOnly: true,
  });
};
```

## 45. Anti-patterns

```typescript
// ❌ Не идемпотентный transformer — бесконечный цикл
transformValue(path.field, (v) => `prefix-${v}`); // f(f(x)) = "prefix-prefix-x" ≠ f(x)

// ✅ Делайте guard внутри transformer
transformValue(path.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));
```

```typescript
// ❌ Трансформация ОДНОВРЕМЕННО с syncFields на том же поле
syncFields(path.a, path.b);
transformValue(path.b, (v) => v.toUpperCase());
// При записи в `a` → b получит сырой v, transformValue запишет b обратно,
// syncFields перезапишет a, и т. д.

// ✅ Трансформируйте источник до синхронизации
transformValue(path.a, (v) => v.toUpperCase());
syncFields(path.a, path.b);
```

```typescript
// ❌ Тяжёлая логика в transformer без debounce
transformValue(path.text, (v) => heavyParse(v));

// ✅ Дебаунсим перерасчёт
transformValue(path.text, (v) => heavyParse(v), { debounce: 250 });
```

```typescript
// ❌ Использование transformValue для производных полей
transformValue(path.fullName, () => `${form.firstName} ${form.lastName}`);
// transformValue не имеет доступа к форме, только к value одного поля

// ✅ Для зависимостей от других полей — computeFrom
computeFrom([path.firstName, path.lastName], path.fullName, (v) => `${v.firstName} ${v.lastName}`);
```

## 46. Troubleshooting

**Q: Цикл «Cycle detected» при transformValue.**
A: 99% случаев — неидемпотентный transformer. Проверьте: `transformer(transformer(x)) === transformer(x)` для типичных значений. Добавьте guard «уже преобразовано».

**Q: Трансформация не применяется при загрузке через `patchValue`.**
A: Если стоит `onUserChangeOnly: true` — это by design. Если не стоит, проверьте, что behavior зарегистрирован (передан в `createForm({ behavior })`) и форма не пересоздаётся при каждом рендере (используйте `useMemo`).

**Q: Каретка input прыгает в начало строки при наборе.**
A: Симптом — `setValue` в transformValue вызывает re-render. Лучшие практики: (1) `debounce: 100…200`; (2) держать преобразование как можно ближе к идемпотентному; (3) форматирование с динамическими разделителями (телефон, кредитка) лучше делать через `<InputMask>` из `@reformer/ui-kit`, а не через transformValue.

**Q: Хочу трансформировать только при blur, а не на каждом keystroke.**
A: Стандартная опция отсутствует, но эффект достигается через `onUserChangeOnly: true` + большой `debounce` (500-700 мс) — пользователь успевает закончить ввод. Альтернатива — слушать blur вручную через React-обработчик.

**Q: Как сделать переиспользуемые трансформации?**
A: Используйте `createTransformer<T>((value) => …)` — возвращает функцию вида `(path) => void`, которую можно применять в любых behaviour-схемах. См. готовый набор `transformers.{toUpperCase, trim, digitsOnly, roundTo2}`.

## 47. See also

- [24-sync-fields.md](./24-sync-fields.md) — порядок применения с syncFields
- [23-copy-from.md](./23-copy-from.md) — `transform`-опция при копировании
- [22-cycle-detection.md](./22-cycle-detection.md) — про идемпотентность
- [16-ui-components.md](./16-ui-components.md) — `<InputMask>` для тяжёлого форматирования

## 48. Purpose

`revalidateWhen` перезапускает валидацию **target-поля**, когда изменяется любое из **trigger-полей**. Применяется, когда правило target зависит от значений других полей (`amount <= maxAmount`, `confirmPassword === password`, `initialPayment >= propertyValue * 0.2`). Без `revalidateWhen` валидатор target не получит сигнала о смене триггера и сохранит устаревшую ошибку. Не дублирует логику валидации — только просит перезапустить уже прописанные правила.

## 49. API

```typescript
function revalidateWhen<TForm>(
  target: FieldPathNode<TForm, FormValue>,
  triggers: FieldPathNode<TForm, FormValue>[],
  options?: RevalidateWhenOptions
): void;

interface RevalidateWhenOptions {
  /** Debounce в миллисекундах. */
  debounce?: number;
}
```

Внутри подписывается на `value.value` каждого `triggers[i]` и вызывает `target.validate()`. Если триггер исчез из формы (path не резолвится) — он игнорируется молча.

## 50. Examples

### Базовый сценарий — amount ≤ maxAmount

```typescript
import { revalidateWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
import { max } from '@reformer/core/validators';
import type { FieldPath } from '@reformer/core';

interface PaymentForm {
  maxAmount: number;
  amount: number;
}

export const paymentValidation = (path: FieldPath<PaymentForm>) => {
  // ВНИМАНИЕ: max() сейчас принимает константу — для динамики комбинируйте с custom-валидатором
  max(path.amount, 1000);
};

export const paymentBehavior: BehaviorSchemaFn<PaymentForm> = (path) => {
  // Когда maxAmount меняется — перезапускаем валидацию amount
  revalidateWhen(path.amount, [path.maxAmount]);
};
```

Source: `BehaviorsExamples.tsx:252` (monorepo example).

### Несколько триггеров — initialPayment vs propertyValue/loanAmount

```typescript
import { revalidateWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface MortgageForm {
  propertyValue: number;
  loanAmount: number;
  initialPayment: number;
}

export const mortgageBehavior: BehaviorSchemaFn<MortgageForm> = (path) => {
  // initialPayment зависит и от propertyValue (минимум 20%), и от loanAmount (= propertyValue - loanAmount)
  revalidateWhen(path.initialPayment, [path.propertyValue, path.loanAmount], {
    debounce: 300,
  });
};
```

Source: `credit-application-behavior.ts:242-245` (monorepo example).

### Парная перевалидация — confirmPassword

```typescript
import { revalidateWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
import { equalTo } from '@reformer/core/validators';
import type { FieldPath } from '@reformer/core';

interface RegistrationForm {
  password: string;
  confirmPassword: string;
}

export const registrationValidation = (path: FieldPath<RegistrationForm>) => {
  equalTo(path.confirmPassword, path.password, { message: 'Пароли не совпадают' });
};

export const registrationBehavior: BehaviorSchemaFn<RegistrationForm> = (path) => {
  // Если пользователь сначала ввёл confirm, потом меняет password — перевалидируем confirm
  revalidateWhen(path.confirmPassword, [path.password]);
};
```

### Edge case — перевалидация после async copyFrom

```typescript
import { revalidateWhen, copyFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface CheckoutForm {
  sameEmail: boolean;
  email: string;
  emailAdditional: string;
}

export const checkoutBehavior: BehaviorSchemaFn<CheckoutForm> = (path) => {
  copyFrom(path.email, path.emailAdditional, {
    when: (form) => form.sameEmail === true,
  });

  // После копирования валидаторы emailAdditional не догонят без явного триггера
  revalidateWhen(path.emailAdditional, [path.email, path.sameEmail], { debounce: 100 });
};
```

## 51. Anti-patterns

```typescript
// ❌ Перевалидация ВСЕХ полей при изменении одного — оверкилл
revalidateWhen(path.field1, [path.x]);
revalidateWhen(path.field2, [path.x]);
revalidateWhen(path.field3, [path.x]);
// плюс field1/2/3 валидируются на собственное изменение → дублирующие пробеги

// ✅ Перевалидируйте только то, чьё правило ЗАВИСИТ от триггера
revalidateWhen(path.totalCheck, [path.x]); // одно поле
```

```typescript
// ❌ revalidateWhen вместо проверки в самом валидаторе
revalidateWhen(path.amount, [path.maxAmount]);
// но в схеме валидации стоит max(path.amount, 1000) — константа, не динамика

// ✅ Сначала custom-валидатор, читающий form, потом revalidateWhen
custom(path.amount, (value, form) => value <= form.maxAmount, { message: '...' });
revalidateWhen(path.amount, [path.maxAmount]);
```

```typescript
// ❌ Триггер == target
revalidateWhen(path.amount, [path.amount]);
// поле и так валидируется при изменении само по себе

// ✅ Триггеры — другие поля
revalidateWhen(path.amount, [path.maxAmount, path.discount]);
```

```typescript
// ❌ revalidateWhen на async-валидаторах без debounce
revalidateWhen(path.username, [path.email]);
// каждый keystroke email → fetch /username/check

// ✅ Debounce обязателен для async
revalidateWhen(path.username, [path.email], { debounce: 500 });
```

## 52. Troubleshooting

**Q: Ошибка target не пропадает после изменения триггера.**
A: Проверьте, что (1) target имеет валидатор, который реально использует значение триггера (custom + read from form); (2) вы передали именно `path.trigger`, а не строку; (3) форма создана с текущим behavior (нет stale `useMemo`).

**Q: Валидация запускается слишком часто.**
A: Передайте `debounce: 200…500`. Для async-валидаторов debounce обязателен — иначе сервер получает шквал запросов.

**Q: Цикл при `revalidateWhen` + `transformValue` на target.**
A: `transformValue` модифицирует target → запускает свою validate → revalidateWhen видит изменение target… `revalidateWhen` подписан только на triggers, а не на target, поэтому цикла быть не должно. Если вы видите цикл — проверьте, не входит ли target в `triggers`.

**Q: Триггер — это вложенное поле в группе. Передаю `path.address.region` — не работает.**
A: `path.address.region` корректен, но если `address` создаётся как `FormProxy` динамически (например, через `apply`), убедитесь, что behavior вызывается **внутри** `apply`-callback с правильным path, а не из родителя.

**Q: Хочу перевалидировать после `form.patchValue(...)`.**
A: `revalidateWhen` сработает, потому что `patchValue` меняет values триггеров. Но если ваша задача — единый прогон валидации после загрузки данных, проще вызвать `await form.validate()` сразу после `patchValue` (см. [29-async-preload.md](./29-async-preload.md)).

## 53. See also

- [03-api-signatures.md](./03-api-signatures.md) — встроенные валидаторы и custom
- [28-submit-and-reset.md](./28-submit-and-reset.md) — `form.validate()` целиком
- [11-async-watchfield.md](./11-async-watchfield.md) — низкоуровневая подписка на изменения
- [05-common-mistakes.md](./05-common-mistakes.md) — почему «всё перевалидировать» — антипаттерн

## 54. Purpose

Раздел описывает канонический submit-флоу `@reformer/core`: «отметить все поля как `touched` → запустить полную валидацию → проверить `valid` → достать `getValue()` → сделать запрос → `reset()`». Внутри submit-handler-а доступны `form.pending` (включая async-валидаторы), `form.invalid` (для блокировки кнопки), `form.errors` (для итогового вывода). Использование вне React-компонента — те же шаги, без разницы. `form.reset()` возвращает все поля к initial values + чистит `dirty/touched/errors`.

## 55. API

```typescript
// Прокидывается всеми FormProxy и GroupNode:

interface FormLifecycleAPI<T> {
  /** Помечает каждое поле как touched (показ ошибок в UI). */
  markAsTouched(): void;

  /** Запускает все валидаторы (включая async). Возвращает Promise<boolean> — итоговый valid. */
  validate(): Promise<boolean>;

  /** Реактивные сигналы. */
  valid: Signal<boolean>;
  invalid: Signal<boolean>;
  pending: Signal<boolean>; // true пока крутятся async-валидаторы
  dirty: Signal<boolean>;
  touched: Signal<boolean>;
  errors: Signal<ValidationError[]>;

  /** Снимает все значения формы (deep). Возвращает T. */
  getValue(): T;

  /** Возвращает ВСЕ поля к initial values + чистит dirty/touched/errors/disabled. */
  reset(): void;
}
```

`form.valid.value` сразу после `markAsTouched()` без `await validate()` может вернуть устаревшее значение — обязательно **`await form.validate()`** перед чтением `valid`.

## 56. Examples

### Базовый submit-handler

```tsx
import { useMemo } from 'react';
import { createForm, type FormProxy } from '@reformer/core';

interface RegistrationFormData {
  username: string;
  email: string;
  password: string;
}

function RegistrationForm() {
  const form = useMemo(() => createForm<RegistrationFormData>({
    form: { /* schema */ },
    validation: /* validation */,
    behavior: /* behavior */,
  }), []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    // Шаг 1: показать ошибки на всех полях
    form.markAsTouched();

    // Шаг 2: дождаться async валидации
    await form.validate();

    // Шаг 3: проверить итог
    if (!form.valid.value) {
      return;
    }

    // Шаг 4: достать чистые данные и отправить
    const payload = form.getValue();
    const response = await fetch('/api/v1/auth/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (response.ok) {
      form.reset(); // Шаг 5: чистый старт после успеха
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* поля */}
      <button
        type="submit"
        disabled={form.invalid.value || form.pending.value}
      >
        {form.pending.value ? 'Проверка...' : 'Отправить'}
      </button>
    </form>
  );
}
```

Source: `RegistrationForm.tsx:122-150` (monorepo example).

### Submit с error-handling и сохранением значений при ошибке

```tsx
async function handleSubmit(e: React.FormEvent) {
  e.preventDefault();

  form.markAsTouched();
  const isValid = await form.validate();

  if (!isValid) {
    return;
  }

  try {
    const payload = form.getValue();
    const response = await api.register(payload);

    if (response.success) {
      // Сбрасываем только при успехе
      form.reset();
      navigate('/welcome');
    } else {
      // Сервер вернул бизнес-ошибку — НЕ сбрасываем форму, показываем ошибку
      // Также не сбрасываем при сетевой ошибке.
      form.username.setErrors([{ code: 'taken', message: response.message }]);
    }
  } catch (error) {
    // Сеть/неожиданная ошибка — оставляем значения, чтобы пользователь не вводил заново
    showToast(`Ошибка сети: ${(error as Error).message}`);
  }
}
```

### Reset с подтверждением + ручная очистка одного поля

```tsx
function ActionButtons({ form }: { form: FormProxy<RegistrationFormData> }) {
  const handleReset = () => {
    if (!form.dirty.value) {
      return; // нечего сбрасывать
    }
    if (confirm('Очистить форму? Несохранённые изменения будут потеряны.')) {
      form.reset();
    }
  };

  const clearJustPassword = () => {
    // Сброс одного поля (без затрагивания остальных)
    form.password.setValue('');
    form.password.markAsPristine();
    form.password.markAsUntouched();
  };

  return (
    <>
      <button type="button" onClick={handleReset} disabled={!form.dirty.value}>
        Очистить
      </button>
      <button type="button" onClick={clearJustPassword}>
        Очистить только пароль
      </button>
    </>
  );
}
```

### Использование вне React (server action / Node)

```typescript
import { createForm } from '@reformer/core';

async function processFormPayload(rawData: Partial<MyForm>) {
  const form = createForm<MyForm>({
    form: schema,
    validation,
    behavior,
  });

  // Загружаем данные через patchValue (как initial state)
  form.patchValue(rawData);

  form.markAsTouched();
  const isValid = await form.validate();

  if (!isValid) {
    return { ok: false, errors: form.errors.value };
  }

  return { ok: true, data: form.getValue() };
}
```

## 57. Anti-patterns

```typescript
// ❌ Чтение form.valid без await form.validate()
const handleSubmit = (e) => {
  e.preventDefault();
  form.markAsTouched();
  if (form.valid.value) {
    // async-валидаторы ещё не завершились!
    submit();
  }
};

// ✅ Дождаться validate()
const handleSubmit = async (e) => {
  e.preventDefault();
  form.markAsTouched();
  await form.validate();
  if (form.valid.value) submit();
};
```

```typescript
// ❌ form.reset() в onSubmit ДО ответа сервера — потеря данных при ошибке
const handleSubmit = async (e) => {
  e.preventDefault();
  await api.send(form.getValue());
  form.reset(); // Если api.send бросит, юзер потеряет ввод
};

// ✅ Reset только после успеха
const handleSubmit = async (e) => {
  e.preventDefault();
  try {
    await api.send(form.getValue());
    form.reset();
  } catch (err) {
    showError(err);
  }
};
```

```typescript
// ❌ Disabled только по invalid (без pending) — кнопка кликабельна во время async
<button disabled={form.invalid.value}>Submit</button>
// пользователь может нажать пока async-валидатор крутится

// ✅ Disabled = invalid OR pending
<button disabled={form.invalid.value || form.pending.value}>
  {form.pending.value ? 'Проверка…' : 'Submit'}
</button>
```

```typescript
// ❌ Двойная отправка — отсутствие guard'а isSubmitting
const handleSubmit = async () => {
  await form.validate();
  if (form.valid.value) await api.send(form.getValue());
};

// ✅ Локальный флаг + disabled
const [isSubmitting, setSubmitting] = useState(false);
const handleSubmit = async () => {
  if (isSubmitting) return;
  setSubmitting(true);
  try {
    await form.validate();
    if (form.valid.value) await api.send(form.getValue());
  } finally {
    setSubmitting(false);
  }
};
```

```typescript
// ❌ Сброс через setValue полей по очереди — дорого и теряет dirty/touched
form.username.setValue('');
form.email.setValue('');
form.password.setValue('');
// errors остаются, dirty флаги не очищены

// ✅ form.reset() делает всё одним вызовом
form.reset();
```

## 58. Troubleshooting

**Q: `form.valid.value` всегда `false` сразу после `markAsTouched()`.**
A: Без `await form.validate()` async-валидаторы возвращают `pending`. Структура: `markAsTouched()` (sync) → `await form.validate()` (ждём) → `form.valid.value` (актуально).

**Q: После `reset()` поля сбросились, но в UI остались старые ошибки.**
A: Проверьте, что компонент подписан на `errors`/`touched` через `useFormControl(field)` — он сам перерисуется. Если используете кастомный hook, не забудьте подписаться на `field.errors.value`.

**Q: `form.pending.value` залип в `true`.**
A: Один из async-валидаторов выкинул исключение без `try/catch` — реактивный счётчик pending не уменьшился. Оборачивайте `validator: async (value) => { try { … } catch { return null; } }`. Также проверьте: не запускаете ли `form.validate()` параллельно (несколько одновременных вызовов).

**Q: Хочу submit без markAsTouched — оставить «зелёную» форму.**
A: Можно. `markAsTouched` нужен только чтобы UI **показал** ошибки. Если вы рендерите ошибки сами (через `form.errors.value`), достаточно `await form.validate(); if (form.valid.value) …`.

**Q: Reset не возвращает initial values — поля очищаются в null.**
A: Initial values в схеме задаются через `value: …`. Если вы передавали `value: undefined`, reset вернёт `undefined`/`null`. Также `patchValue(...)` НЕ меняет initial values — только текущие; reset вернёт к тем, что были в схеме.

**Q: Disabled-поля участвуют в submit-данных?**
A: `getValue()` возвращает значения **всех** полей, включая disabled, в том виде, в котором они есть. Если хотите вырезать disabled — фильтруйте вручную после `getValue()` или используйте `enableWhen({ resetOnDisable: true })`, чтобы при disable в поле возвращалось initial.

## 59. See also

- [29-async-preload.md](./29-async-preload.md) — initial values и preload через external hook
- [11-async-watchfield.md](./11-async-watchfield.md) — async-валидация и `pending`
- [03-api-signatures.md](./03-api-signatures.md) — полные сигнатуры FormProxy/GroupNode
- [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки в submit-флоу

## 60. Purpose

Раздел про загрузку формы данными с сервера: (1) **initial values** в схеме (для дефолтов и mock-данных без API); (2) **patchValue** для частичного обновления существующей формы; (3) **external React-hook** (`useLoadCreditApplication`) для full-blown async preload — параллельный fetch заявки + справочников, обработка ошибок, race-condition guard через `applicationId` в deps. Динамические `componentProps` (опции селектов и т. п.) обновляются через `queueMicrotask`, чтобы не пересечься с реактивными эффектами от `patchValue`.

## 61. API

```typescript
// FormProxy / GroupNode:

interface PreloadAPI<T> {
  /** Полная замена value. Перетирает всё. */
  setValue(value: T): void;

  /** Частичный мердж. Не трогает поля, отсутствующие в payload. Используется для preload. */
  patchValue(value: Partial<T>): void;

  /** Initial values из схемы. reset() возвращает к ним. */
  reset(): void;

  /** Динамическое обновление componentProps конкретного поля. */
  field.updateComponentProps(props: Record<string, unknown>): void;
}
```

В схеме `FormSchema<T>` каждое поле принимает `value: TFieldValue` — это initial value, в которое поле возвращается при `reset()`.

## 62. Examples

### Initial values в схеме

```typescript
import { createForm, type FormSchema } from '@reformer/core';
import { Input, Select } from '@reformer/ui-kit';

interface ProfileForm {
  username: string;
  language: 'ru' | 'en';
  marketing: boolean;
}

const schema: FormSchema<ProfileForm> = {
  username: {
    value: '', // initial
    component: Input,
    componentProps: { label: 'Username' },
  },
  language: {
    value: 'ru', // дефолт «Русский»
    component: Select,
    componentProps: {
      label: 'Язык',
      options: [
        { value: 'ru', label: 'Русский' },
        { value: 'en', label: 'English' },
      ],
    },
  },
  marketing: {
    value: true,
    component: Input,
    componentProps: { type: 'checkbox' },
  },
};

const form = createForm({ form: schema });
// form.getValue() === { username: '', language: 'ru', marketing: true }
// после правок: form.reset() возвращает к этим же значениям
```

### Async preload через external hook + patchValue

```tsx
import { useEffect, useState } from 'react';
import type { FormProxy } from '@reformer/core';

interface LoadingState {
  isLoading: boolean;
  error: string | null;
}

export function useLoadCreditApplication(
  form: FormProxy<CreditApplicationForm>,
  applicationId: string | null
): LoadingState {
  const [state, setState] = useState<LoadingState>({
    isLoading: !!applicationId,
    error: null,
  });

  useEffect(() => {
    if (!applicationId) {
      setState({ isLoading: false, error: null });
      return;
    }

    let cancelled = false;

    (async () => {
      setState({ isLoading: true, error: null });

      try {
        const [appResp, dictsResp] = await Promise.all([
          fetchCreditApplication(applicationId),
          fetchDictionaries(),
        ]);

        // Race-guard: пользователь успел сменить applicationId или unmount
        if (cancelled) return;

        if (appResp.status !== 200 || dictsResp.status !== 200) {
          throw new Error('Сервер вернул ошибку');
        }

        // patchValue для полей формы
        form.patchValue(appResp.data);

        // Динамические componentProps — через queueMicrotask, чтобы дождаться
        // окончания реактивных эффектов от patchValue
        queueMicrotask(() => {
          if (cancelled) return;
          form.registrationAddress.city.updateComponentProps({
            options: dictsResp.data.cities,
          });
          form.properties?.forEach((prop) =>
            prop.type.updateComponentProps({ options: dictsResp.data.propertyTypes })
          );
        });

        setState({ isLoading: false, error: null });
      } catch (err) {
        if (cancelled) return;
        setState({
          isLoading: false,
          error: err instanceof Error ? err.message : 'Неизвестная ошибка',
        });
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [applicationId]); // form стабилен (создан через useMemo)

  return state;
}
```

Source: `useLoadCreditApplication.ts` (monorepo example).

### Использование hook'а в компоненте

```tsx
import { useMemo } from 'react';
import { LoadingState } from './components/LoadingState';
import { ErrorState } from './components/ErrorState';

function CreditApplicationForm() {
  const form = useMemo(() => createCreditApplicationForm(), []);
  const { isLoading, error } = useLoadCreditApplication(form, '1'); // ID заявки

  if (isLoading) return <LoadingState />;
  if (error) return <ErrorState error={error} onRetry={() => location.reload()} />;

  return <FormWizard form={form} steps={STEPS} onSubmit={onSubmit} />;
}
```

Source: `CreditApplicationForm.tsx:50-66` (monorepo example).

### Preload через behavior (когда сервер отвечает на изменение поля)

```typescript
import { watchField, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface AddressForm {
  region: string;
  city: string;
}

export const addressBehavior: BehaviorSchemaFn<AddressForm> = (path) => {
  // Загрузка списка городов при выборе региона
  watchField(
    path.region,
    async (region, ctx) => {
      if (!region) {
        ctx.form.city.updateComponentProps({ options: [] });
        return;
      }

      try {
        const { data: cities } = await fetchCities(region);
        ctx.form.city.updateComponentProps({ options: cities });
      } catch (err) {
        console.error('Failed to load cities:', err);
        ctx.form.city.updateComponentProps({ options: [] });
      }
    },
    { immediate: false, debounce: 300 }
  );
};
```

Source: `address-behavior.ts` (monorepo example).

## 63. Anti-patterns

```typescript
// ❌ form пересоздаётся при каждом рендере → preload запускается каждый раз
function MyForm() {
  const form = createForm({ form: schema }); // КАЖДЫЙ рендер!
  useLoadCreditApplication(form, '1');
  return …;
}

// ✅ Стабильная ссылка через useMemo
function MyForm() {
  const form = useMemo(() => createForm({ form: schema }), []);
  useLoadCreditApplication(form, '1');
  return …;
}
```

```typescript
// ❌ updateComponentProps синхронно после patchValue → "Cycle detected"
form.patchValue(data);
form.region.updateComponentProps({ options: [...] }); // bang

// ✅ queueMicrotask, чтобы реактивные эффекты от patchValue завершились
form.patchValue(data);
queueMicrotask(() => {
  form.region.updateComponentProps({ options: [...] });
});
```

```typescript
// ❌ Полная замена через setValue вместо patchValue
form.setValue(partialData); // partialData может не содержать все поля → перетирает в undefined

// ✅ patchValue — partial update, сохраняет необъявленные поля
form.patchValue(partialData);
```

```typescript
// ❌ Отсутствие race-guard в async useEffect
useEffect(() => {
  fetchData(id).then((data) => form.patchValue(data));
}, [id]);
// Если id быстро поменялся, старый response перезапишет новый

// ✅ Cleanup-флаг
useEffect(() => {
  let cancelled = false;
  fetchData(id).then((data) => {
    if (!cancelled) form.patchValue(data);
  });
  return () => {
    cancelled = true;
  };
}, [id]);
```

```typescript
// ❌ Initial values задаются через patchValue после createForm — reset() их не помнит
const form = createForm({ form: schema });
form.patchValue({ username: 'default' });
form.reset(); // username станет '' (из schema), а не 'default'

// ✅ Если значение должно быть initial, кладите его в schema.value
const schema = { username: { value: 'default', component: Input } };
```

## 64. Troubleshooting

**Q: После `patchValue` ошибки валидации не показываются.**
A: `patchValue` НЕ trigger'ит `markAsTouched`. Если хотите сразу видеть ошибки — после `patchValue` вызовите `form.markAsTouched(); await form.validate();`.

**Q: Опции в `<Select>` не появляются после `updateComponentProps`.**
A: (1) Убедитесь, что вызов обёрнут в `queueMicrotask` после patch'ей; (2) проверьте, что компонент Select подписан на изменения через `useFormControl` (не использует props напрямую без подписки); (3) для FormArray — пройдитесь `forEach` по элементам и обновите для каждого.

**Q: `form.patchValue` не работает для FormArray.**
A: Работает, но требует, чтобы массив существовал. Если в schema у вас `arr: { value: [] }`, `patchValue({ arr: [item1, item2] })` создаст элементы. Опции внутри элементов придётся обновить через `forEach` отдельно (см. example).

**Q: Двойной запрос при mount + StrictMode.**
A: StrictMode вызывает effect дважды. Race-guard через `cancelled` флаг (см. пример) корректно отменяет первый запрос. Дополнительно проверьте, что `applicationId` стабилен между рендерами.

**Q: Хочу прелоад без отдельного hook'а — прямо в behavior через `watchField` на `id`.**
A: Можно, но behavior запускается в момент создания формы и не может вернуть loading state. Лучше использовать external hook + `patchValue`. Behavior подходит для динамики **после** preload (загрузка городов при смене региона и т. п.).

**Q: При `reset()` теряются данные, загруженные с сервера.**
A: Это by design — `reset()` возвращает к initial values из schema, не к последнему `patchValue`. Если нужно «сбросить к данным с сервера», храните snapshot и используйте `form.setValue(snapshot)` вместо `reset()`.

## 65. See also

- [28-submit-and-reset.md](./28-submit-and-reset.md) — обратная сторона жизненного цикла
- [11-async-watchfield.md](./11-async-watchfield.md) — `watchField` для динамики после preload
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `queueMicrotask` нужен
- [16-ui-components.md](./16-ui-components.md) — `updateComponentProps` для динамических опций

## 66. 30. TYPE-SAFETY RECIPES

Idiomatic patterns that keep generated code free of `any`, `as` casts, and
implicit-any cascades. Use these as defaults; the legacy
`(path: any)` workaround in `04-common-patterns.md` is for TS2589 only.

### Recipe 1 — Imports (root cause prevention)

Types from `@reformer/core`, functions from submodules. Never put types in
`/validators` / `/behaviors` imports.

```typescript
import {
  createForm,
  type FormProxy,
  type FormSchema,
  type FieldPath,
  type ValidationSchemaFn,
  type BehaviorSchemaFn,
} from '@reformer/core';
import { required, min, max, applyWhen, apply } from '@reformer/core/validators';
import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
```

### Recipe 2 — Form-shape types as `type`, not `interface`

`Record<string, FormValue>` (= `FormFields`) needs an index signature.
`interface` doesn't supply one implicitly; `type` does — structurally.

```typescript
// ✅ Use type aliases for everything that ends up inside FormProxy<T>:
//    nested groups, array element shapes, and the root form type.
export type PropertyItem = {
  type: 'apartment' | 'house' | 'car';
  description: string;
  estimatedValue: number;
};

export type CreditApplicationForm = {
  loanAmount: number | null;
  properties: PropertyItem[];
  // ...
};
```

This avoids "Type 'PropertyItem' does not satisfy the constraint
'FormFields'" on explicit generics like `<FormArraySection<PropertyItem>>`,
and "Type 'PropertyItem' is not assignable to type 'Partial<FormFields>'"
on `initialValue` props.

### Recipe 3 — Validation/behavior callbacks: typed signatures

Use `ValidationSchemaFn<T>` / `BehaviorSchemaFn<T>` directly. `path` is
inferred; `applyWhen`-callback `p` is inferred from the function signature
(`validationFn: (path: FieldPath<TForm>) => void`). No `(path: any)` needed.

```typescript
const step1: ValidationSchemaFn<MyForm> = (path) => {
  required(path.loanAmount);
  min(path.loanAmount, 50000);

  applyWhen(
    path.loanType,
    (loanType) => loanType === 'mortgage',
    (p) => {
      required(p.propertyValue);
      min(p.propertyValue, 1_000_000);
    }
  );
};

const fullValidation: ValidationSchemaFn<MyForm> = (path) => {
  step1(path);
  apply(path.personalData, personalDataValidation); // delegate to nested
};
```

### Recipe 4 — `computeFrom` callback: annotate the destructured arg

`computeFn: (values: TForm) => TTarget` exposes the full form, not a
narrowed Pick. Without annotation TS infers fields as `unknown`. With
annotation, fields are typed exactly as declared on `T`.

```typescript
computeFrom(
  [path.loanAmount, path.loanTerm, path.interestRate],
  path.monthlyPayment,
  ({ loanAmount, loanTerm, interestRate }: MyForm) =>
    annuityMonthly(loanAmount ?? 0, loanTerm ?? 0, interestRate ?? 0)
);

// Group-node subscription for nested reads — no leaf-by-leaf:
computeFrom([path.personalData], path.fullName, ({ personalData }: MyForm) =>
  [personalData.firstName, personalData.lastName].filter(Boolean).join(' ')
);
```

### Recipe 5 — `null` vs `undefined` for optional numbers/strings

Both work. `null` is conventional for "user cleared the field" in form
libraries; built-in validators (`min`, `max`, `minLength`, `maxLength`,
`minDate`, `maxDate`, `minAge`, `maxAge`) accept `T | null | undefined`
and skip empty values internally — you don't need to wrap them in
`if (value != null)` guards.

```typescript
export type CreditForm = {
  loanAmount: number | null; // OK — min(path.loanAmount, 50000) accepts null
  loanPurpose: string | null; // OK — minLength accepts null
  birthDate: string | null; // OK — minDate/maxAge accept null
};
```

### Recipe 6 — `FormArraySection` with array-element generics

Pass the element type explicitly when TS cannot infer it from the
`control` union (FieldPathNode in the union widens T to FormFields).

```typescript
<FormArraySection<PropertyItem>
  control={control.properties}
  itemComponent={PropertyItemForm}
  initialValue={createPropertyItem()}  // Partial<PropertyItem> — checked
/>
```

If the element type is declared as `type` (Recipe 2), `<PropertyItem>`
satisfies the `extends FormFields` constraint structurally.

### Recipe 7 — `FormWizard` with form-typed ref

`FormWizard` infers `T` from the `form` prop, but JSX cannot accept a
generic at the call site. Declare the ref with the explicit form type;
the constraint `T extends Record<string, any>` allows nullable fields
(`number | null`).

```typescript
const navRef = useRef<FormWizardHandle<MyForm>>(null);

<FormWizard
  ref={navRef}
  form={form}
  config={{ stepValidations, fullValidation }}
  steps={STEPS}
  onSubmit={onSubmit}
/>
```

### Recipe 8 — enum-typed default values (union literal widening)

Когда default `value` поля — литерал union-type, TS widens его до `string`,
ломая `FormSchema<T>` assignability:

```typescript
type Gender = 'male' | 'female';

// ❌ TS error: '"male"' is not assignable to 'Gender | null'
const schema: FormSchema<{ gender: Gender }> = {
  gender: {
    value: 'male', // widens to string
    component: RadioGroup,
    componentProps: { options: [...] },
  },
};
```

**Fix** — `satisfies FieldConfig<UnionType>` сужает без `as`-каста:

```typescript
gender: {
  value: 'male',
  component: RadioGroup,
  componentProps: { options: [...] },
} satisfies FieldConfig<Gender>,
```

Альтернативы:

- `value: 'male' as Gender` — works, но `as`-каст. Избегай (см. anti-patterns).
- `value: null` (если `Gender | null` допустим) — нет widening проблемы, но требует
  null-check в render/validation.

Применяется к любому union-полю: `loanType`, `employmentStatus`, `maritalStatus`,
`propertyType`, и т.п. Симптом без этого fix'а — misleading TS2769
"`'form' does not exist in FormSchema<...>`" (см. `05-common-mistakes.md`).

### Anti-patterns to avoid

- `import { type ValidationSchemaFn } from '@reformer/core/validators'` →
  TS2614, cascades to 30+ implicit-any errors.
- `(values as PersonalData | undefined)` / `(loanAmount as number | null)`
  inside `computeFrom` → use Recipe 4 instead.
- `(path: any)` annotation on validation/behavior callbacks → only valid as
  TS2589 workaround for 6+ levels of nesting; not the default.
- `interface MyForm { ... }` for form-shape types → see Recipe 2.
- `as never` cast on `computeFrom` target — never necessary; if you reach
  for it, you have one of the issues above.

## 67. 31. ASYNC VALIDATOR WITH DEBOUNCE

Для проверок типа «уникальность email», «валидация INN через API», «проверка
адреса по DaData» используй `asyncValidators` в `FieldConfig`:

```ts
import { type FieldConfig, type FormSchema, type AsyncValidatorFn } from '@reformer/core';
import { required, email } from '@reformer/core/validators';

const checkEmailUnique: AsyncValidatorFn<string> = async (value) => {
  if (!value) return null; // empty = valid (sync `required` separately)

  try {
    const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`);
    const { available } = (await res.json()) as { available: boolean };
    return available ? null : { code: 'email-taken', message: 'Email уже зарегистрирован' };
  } catch {
    return { code: 'check-failed', message: 'Не удалось проверить email' };
  }
};

const schema: FormSchema<{ email: string }> = {
  email: {
    value: '',
    component: Input,
    validators: [required(), email()], // sync first
    asyncValidators: [checkEmailUnique], // async after sync passed
    debounce: 500, // debounce input → API (поле `debounce`, не `asyncDebounceMs`)
  },
};
```

### Lifecycle

1. На каждое `setValue` запускается sync `validators` (`required`, `email`)
2. Если sync passed — стартует таймер `debounce`
3. По истечении debounce и стабильном value — вызывается каждый `asyncValidator`
4. Во время async-проверки `useFormControl(...).pending === true` — UI может показать спиннер
5. Результат записывается в `errors` поля

### UI integration

`FormField` из `@reformer/ui-kit` автоматически рендерит `<span>Проверка...</span>`
когда `pending === true`. Если рендеришь сам — используй:

```tsx
const { pending, errors } = useFormControl(form.email);
return pending ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
```

### Common patterns

- **Debounce 500ms** — баланс между UX и API rate-limit. Для дорогих API увеличивай
  до 1000-2000ms.
- **Cancellation** — если `setValue` приходит во время выполнения предыдущего async,
  ReFormer автоматически отбрасывает результат старого вызова. Не нужно вручную
  abort'ить fetch (но reasonable practice — поддержать `AbortSignal` через
  `ctx.signal` если есть).
- **Cross-field async** — для валидаций типа «дата начала > дата окончания» используй
  sync `validators` с `applyWhen`, а не `asyncValidators`.

### See also

- [11-async-watchfield.md](11-async-watchfield.md) — async для `watchField`, не валидаторов
- [29-async-preload.md](29-async-preload.md) — async preload данных в init формы
- API: `validateAsync(field): Promise<boolean>` — manual trigger, обычно не нужен

## 68. 32. ASYNC OPTIONS LOADING

Для динамической подгрузки опций dropdown'а в зависимости от значения другого поля
(например: `region` → `city options`, `carBrand` → `carModel options`) — используй
`watchField` + `updateComponentProps({ options })`:

```ts
import { watchField } from '@reformer/core/behaviors';
import type { BehaviorSchemaFn } from '@reformer/core';

type CityOption = { value: string; label: string };

async function fetchCitiesByRegion(region: string): Promise<CityOption[]> {
  const res = await fetch(`/api/cities?region=${encodeURIComponent(region)}`);
  return res.json();
}

const behavior: BehaviorSchemaFn<MyForm> = (path) => {
  watchField(
    path.registrationAddress.region,
    async (region, ctx) => {
      if (!region) {
        ctx.form.registrationAddress.city.updateComponentProps({ options: [] });
        ctx.form.registrationAddress.city.setValue('');
        return;
      }

      // Set loading state (optional — UI показывает spinner)
      ctx.form.registrationAddress.city.updateComponentProps({
        loading: true,
        options: [],
      });

      try {
        const options = await fetchCitiesByRegion(region);
        ctx.form.registrationAddress.city.updateComponentProps({
          loading: false,
          options,
        });
      } catch {
        ctx.form.registrationAddress.city.updateComponentProps({
          loading: false,
          options: [],
        });
        // optionally — set error на поле через setError
      }
    },
    { debounce: 300 }
  );
};
```

### Lifecycle

1. `watchField` подписан на изменения `path.region`
2. На каждое изменение — debounce 300ms (чтобы не fetch на каждое нажатие клавиши)
3. После debounce — async `fetchCitiesByRegion(region)`
4. Результат — `updateComponentProps({ options })` на target поле
5. UI (Select / Combobox) автоматически обновится через signal на componentProps

### Common patterns

- **Debounce 300-500ms** — баланс между UX и rate-limit
- **Reset target value** — при смене source очисти `setValue('')` чтобы не остался stale выбор
- **Loading state** — `componentProps.loading: true` пока fetch идёт (UI компонент должен это поддержать)
- **Cancellation** — ReFormer **не** отменяет старый fetch автоматически. Если предыдущий fetch ещё идёт, а пришло новое значение — новый запустится параллельно. Может приводить к race. Workaround: использовать `AbortController` через closure:
  ```ts
  let abortController: AbortController | null = null;
  watchField(path.region, async (region, ctx) => {
    abortController?.abort();
    abortController = new AbortController();
    try {
      const opts = await fetchCities(region, { signal: abortController.signal });
      ctx.form.city.updateComponentProps({ options: opts });
    } catch (e) {
      if ((e as Error).name === 'AbortError') return;
      throw e;
    }
  });
  ```

### Initial load (preload at form mount)

Для preload опций при инициализации формы — используй `async-preload` recipe (см. [29-async-preload.md](29-async-preload.md)). Не `watchField` — он не triggers на init.

### See also

- [11-async-watchfield.md](11-async-watchfield.md) — общий паттерн async в `watchField`
- [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
- [31-async-validator-debounce.md](31-async-validator-debounce.md) — для async **валидации** (не options)

## 69. API Reference

_Auto-generated from JSDoc on public exports._

### AbstractRegistry

**Kind:** `class`

Базовый класс для реестров (BehaviorRegistry, ValidationRegistry).

Реализует паттерн Template Method для управления регистрацией:
`beginRegistration()` → `onBeginRegistration()`, `endRegistration()` → `onEndRegistration()`.

**Signature:**
```typescript
export abstract class AbstractRegistry<TRegistration> {
  /** Флаг активной регистрации */ /* … */ }
```

**Examples:**

```typescript
import { AbstractRegistry } from '@reformer/core';

class MyRegistry extends AbstractRegistry<{ name: string }> {
  protected onEndRegistration(items: { name: string }[]) {
    console.log('Registered', items.length);
  }
}
```

_Source: src/core/utils/abstract-registry.ts_

### AnyFunction

**Kind:** `type`

Тип для проверки на функцию в conditional types
Используется вместо Function для type narrowing

**Signature:**
```typescript
export type AnyFunction = (...args: never[]) => unknown;
```

_Source: src/core/types/index.ts_

### ArrayConfig

**Kind:** `interface`

Конфигурация массива

**Signature:**
```typescript
export interface ArrayConfig<T extends FormFields> {
  itemSchema: FormSchema<T>;
  initial?: Partial<T>[];
}
```

_Source: src/core/types/deep-schema.ts_

### ArrayControlState

**Kind:** `interface`

Состояние массива формы, возвращаемое хуком {@link useFormControl} для {@link ArrayNode}.

Содержит реактивные данные массива: значения элементов, длину, состояние валидации
и флаги взаимодействия.

**Signature:**
```typescript
export interface ArrayControlState<T> {
  /**
   * Массив текущих значений всех элементов.
   *
   * @example
   * ```tsx
   * const { value } = useFormControl(phonesArray);
   * console.log(value);
   * // [{ type: 'mobile', number: '+1234567890' }, { type: 'home', number: '+0987654321' }]
   * ```
   */
  value: T[];

  /**
   * Количество элементов в массиве.
   * Эквивалентно value.length, но оптимизировано для реактивности.
   *
   * @example
   * ```tsx
   * const { length } = useFormControl(itemsArray);
   *
   * return (
   *   <div>
   *     <span>Items: {length}</span>
   *     {length >= 10 && <span>Maximum reached</span>}
   *   </div>
   * );
   * ```
   */
  length: number;

  /**
   * Флаг асинхронной валидации.
   * `true` когда выполняется асинхронный валидатор массива или любого элемента.
   */
  pending: boolean;

  /**
   * Массив ошибок валидации уровня массива.
   * Не включает ошибки отдельных элементов.
   *
   * @example
   * ```tsx
   * // Валидатор массива
   * validators.apply(phonesArray, {
   *   validator: (phones) => phones.length >= 1,
   *   message: 'At least one phone required'
   * });
   *
   * // В компоненте
   * const { errors } = useFormControl(phonesArray);
   * // errors содержит ошибку "At least one phone required" если массив пуст
   * ```
   */
  errors: ValidationError[];

  /**
   * Флаг валидности массива и всех его элементов.
   * `true` только когда массив и все вложенные элементы валидны.
   */
  valid: boolean;

  /**
   * Флаг невалидности.
   * `true` когда есть ошибки в массиве или любом элементе.
   */
  invalid: boolean;

  /**
   * Флаг взаимодействия.
   * `true` после взаимодействия с любым элементом массива.
   */
  touched: boolean;

  /**
   * Флаг изменения.
   * `true` когда значение массива отличается от начального.
   *
   * @example
   * ```tsx
   * const { dirty } = useFormControl(itemsArray);
   *
   * return (
   *   <div>
   *     {dirty && <span>* Unsaved changes</span>}
   *     <button disabled={!dirty}>Save</button>
   *   </div>
   * );
   * ```
   */
  dirty: boolean;
}
```

**Examples:**

Список с динамическим добавлением
```tsx
interface Phone {
type: string;
number: string;
}

interface Props {
control: ArrayNode<Phone>;
}

function PhoneList({ control }: Props) {
const { length, valid } = useFormControl(control);

return (
<div>
{control.map((item, index) => (
<PhoneItem
 key={item.id}
 control={item}
 onRemove={() => control.remove(index)}
/>
))}

{length === 0 && <p>No phones added</p>}

<button onClick={() => control.push({ type: 'mobile', number: '' })}>
Add Phone
</button>

{!valid && <p className="error">Please fix phone errors</p>}
</div>
);
}
```

**See also:**
- {@link useFormControl} - хук для получения состояния
- {@link FieldControlState} - состояние для полей

_Source: src/hooks/types.ts_

### ArrayNode

**Kind:** `class`

ArrayNode - массив форм с реактивным состоянием

**Signature:**
```typescript
export class ArrayNode<T extends object> extends FormNode<T[]> {
  // ============================================================================
  // Приватные поля
  // ============================================================================ /* … */ }
```

**Examples:**

```typescript
const array = new ArrayNode({
  title: { value: '', component: Input },
  price: { value: 0, component: Input },
});

array.push({ title: 'Item 1', price: 100 });
array.at(0)?.title.setValue('Updated');
console.log(array.length.value); // 1
```

_Source: src/core/nodes/array-node.ts_

### ArrayNodeLike

**Kind:** `interface`

Интерфейс для узлов, похожих на ArrayNode (с методом at)
Используется для duck typing при обходе путей

**Signature:**
```typescript
export interface ArrayNodeLike {
  at(index: number): FormNode<unknown> | undefined;
  length: unknown;
}
```

_Source: src/core/types/index.ts_

### AsyncValidator

**Kind:** `type`

Чистый асинхронный валидатор поля.

Регистрируется через `validateAsync(path, validator, { debounce })`.

**Signature:**
```typescript
export type AsyncValidator<TForm, TField> = (
  value: TField,
  control: FormProxy<TField>,
  root: FormProxy<TForm>
) => Promise<ValidationError | null>;
```

_Source: src/core/types/validation-schema.ts_

### AsyncValidatorFn

**Kind:** `type`

Асинхронная функция валидации

**Signature:**
```typescript
export type AsyncValidatorFn<T = FormValue> = (
  value: T,
  options?: AsyncValidatorOptions
) => Promise<ValidationError | null>;
```

**Parameters:**
- `value` — - Значение для валидации
- `options` — - Опции валидации (опционально)

**Returns:** Promise с ошибкой валидации или null если значение валидно

**Examples:**

```typescript
// Простой валидатор (без поддержки отмены)
const emailExists: AsyncValidatorFn<string> = async (value) => {
  const exists = await checkEmail(value);
  return exists ? { code: 'exists', message: 'Email already exists' } : null;
};

// Валидатор с поддержкой отмены
const emailExistsAbortable: AsyncValidatorFn<string> = async (value, options) => {
  const exists = await fetch(`/api/check-email?email=${value}`, {
    signal: options?.signal // Передаём signal в fetch для отмены запроса
  });
  return exists ? { code: 'exists', message: 'Email already exists' } : null;
};
```

_Source: src/core/types/index.ts_

### AsyncValidatorOptions

**Kind:** `interface`

Опции для асинхронного валидатора

**Signature:**
```typescript
export interface AsyncValidatorOptions {
  /**
   * AbortSignal для отмены валидации
   * Позволяет отменить асинхронную операцию при новой валидации
   */
  signal?: AbortSignal;
}
```

_Source: src/core/types/index.ts_

### BehaviorCleanup

**Kind:** `type`

Функция отписки от behavior-эффекта.

**Signature:**
```typescript
export type BehaviorCleanup = () => void;
```

_Source: src/core/model/behaviors.ts_

### computeFrom

**Kind:** `function`

Вычисляемое поле: `target = fn(...sourceValues)` при изменении источников.

**Signature:**
```typescript
export function computeFrom<R>(
  sources: ReadonlySignal<any>[],
  target: Signal<R>,
  fn: (...values: any[]) => R,
  options?: { when?: (...values: any[]) => boolean }
): BehaviorCleanup
```

**Parameters:**
- `sources` — Сигналы-источники (`model.$.a`, `model.$.b`).
- `target` — Сигнал-цель (`model.$.total`).
- `fn` — Функция вычисления значения.

**Returns:** Cleanup для отписки.

**Examples:**

```typescript
computeFrom([model.$.price, model.$.qty], model.$.total, (price, qty) => price * qty);
```

_Source: src/core/model/behaviors.ts_

### ConditionFn

**Kind:** `type`

Функция условия для applyWhen.

**Signature:**
```typescript
export type ConditionFn<T> = (value: T) => boolean;
```

_Source: src/core/types/validation-schema.ts_

### ConfigWithSchema

**Kind:** `interface`

Конфиг с полем schema (для ArrayConfig)

**Signature:**
```typescript
export interface ConfigWithSchema {
  schema: unknown;
  initialItems?: unknown[];
}
```

_Source: src/core/types/index.ts_

### ConfigWithValue

**Kind:** `interface`

Конфиг с полем value (для извлечения значений)

**Signature:**
```typescript
export interface ConfigWithValue {
  value: unknown;
}
```

_Source: src/core/types/index.ts_

### copyFrom

**Kind:** `function`

Копирование значения `source → target` (опционально по условию/с трансформом).

**Signature:**
```typescript
export function copyFrom<T>(
  source: ReadonlySignal<T>,
  target: Signal<T>,
  options?: { when?: () => boolean; transform?: (value: T) => T }
): BehaviorCleanup
```

**Examples:**

```typescript
copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail });
```

_Source: src/core/model/behaviors.ts_

### createForm

**Kind:** `function`

Создать форму с полной конфигурацией (form, behavior, validation)

**Signature:**
```typescript
export function createForm<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>;
```

**Parameters:**
- `config` — - Конфигурация формы с полями, поведением и валидацией

**Returns:** Типизированная форма с Proxy-доступом к полям

**Examples:**

```typescript
const form = createForm<UserForm>({
  form: {
    email: { value: '', component: Input },
    password: { value: '', component: Input },
  },
  validation: (path) => {
    required(path.email);
    email(path.email);
    required(path.password);
    minLength(path.password, 8);
  },
});

// TypeScript знает о полях:
form.email.setValue('test@mail.com');
```

_Source: src/core/utils/create-form.ts_

### createFormFromModel

**Kind:** `function`

**Signature:**
```typescript
export function createFormFromModel<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>
```

_Source: src/core/utils/create-form.ts_

### CreateFormFromModelArgs

**Kind:** `interface`

Аргументы createForm под архитектуру M1: данные приходят из {@link FormModel},
конфиг полей (component/componentProps/validators) — из единой схемы.

**Signature:**
```typescript
export interface CreateFormFromModelArgs<T> {
  /** Реактивная модель данных (источник истины значений). */
  model: FormModel<T>;
  /**
   * Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг поля к ноде
   * по идентичности сигнала (`node.value === model.$.path`). Опциональна.
   */
  schema?: unknown;
  /**
   * Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
   * заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
   */
  behavior?: FormBehavior<T>;
}
```

_Source: src/core/utils/create-form.ts_

### createModel

**Kind:** `function`

Создать реактивную модель данных формы (слой M1).

**Signature:**
```typescript
export function createModel<T extends object>(initial: T): FormModel<T>
```

**Parameters:**
- `initial` — Начальные значения (объект). Определяют форму данных и initial-снимок.

**Returns:** 

**Examples:**

```typescript
const model = createModel<{ email: string; tags: string[] }>({ email: '', tags: [] });
model.email = 'a@b.c';
model.$.email.value;        // 'a@b.c' (сигнал)
model.tags.push('x');
model.get();                // { email: 'a@b.c', tags: ['x'] }
```

_Source: src/core/model/form-model.ts_

### disableWhen

**Kind:** `function`

Условное выключение поля (инверсия {@link enableWhen}).

**Signature:**
```typescript
export function disableWhen(
  target: ReadonlySignal<unknown>,
  condition: () => boolean,
  options?: { resetOnDisable?: boolean }
): BehaviorCleanup
```

_Source: src/core/model/behaviors.ts_

### email

**Kind:** `function`

Фабрика валидатора формата email.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function email<TForm = unknown, TField extends string | undefined = string>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.email, email());
validate(path.email, email({ message: 'Введите корректный email' }));
```

_Source: src/core/validation/validators/email.ts_

### enableWhen

**Kind:** `function`

Условное включение поля (state-операция). Резолвит ноду по сигналу-цели через реестр
сигнал→нода (заполняется `createForm`) и вызывает `enable()`/`disable()` (+`reset()` при
`resetOnDisable`). `condition` реактивен (читает свои сигналы модели).

Запись состояния отложена через `runOutsideEffect` (микротаск) для защиты от «Cycle detected».
⚠️ Поле должно быть материализовано в форме (`createForm`) — иначе ноды в реестре нет (например,
элемент массива, который строится per-item).

**Signature:**
```typescript
export function enableWhen(
  target: ReadonlySignal<unknown>,
  condition: () => boolean,
  options?: { resetOnDisable?: boolean }
): BehaviorCleanup
```

**Examples:**

```typescript
enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', { resetOnDisable: true });
```

_Source: src/core/model/behaviors.ts_

### ErrorFilterOptions

**Kind:** `interface`

Опции для фильтрации ошибок в методе getErrors()

**Signature:**
```typescript
export interface ErrorFilterOptions {
  /** Фильтр по коду ошибки */
  code?: string | string[];

  /** Фильтр по сообщению (поддерживает частичное совпадение) */
  message?: string;

  /** Фильтр по параметрам ошибки */
  params?: FormFields;

  /** Кастомный предикат для фильтрации */
  predicate?: (error: ValidationError) => boolean;
}
```

_Source: src/core/types/index.ts_

### ErrorStrategy

**Kind:** `enum`

Стратегия обработки ошибок

Определяет, что делать с ошибкой после логирования

**Signature:**
```typescript
export enum ErrorStrategy {
  /**
   * Пробросить ошибку дальше (throw)
   * Используется когда ошибка критична и должна остановить выполнение
   */
  THROW = 'throw',

  /**
   * Залогировать и проглотить ошибку (продолжить выполнение)
   * Используется когда ошибка не критична
   */
  LOG = 'log',

  /**
   * Конвертировать ошибку в ValidationError
   * Используется в async validators для отображения ошибки валидации пользователю
   */
  CONVERT = 'convert',
}
```

_Source: src/core/utils/error-handler.ts_

### FieldConfig

**Kind:** `interface`

Конфигурация поля

**Signature:**
```typescript
export interface FieldConfig<T> {
  /**
   * Начальное значение-литерал (legacy-путь). Под архитектурой M1 значение приходит из
   * {@link FieldConfig.valueSignal} (сигнал {@link FormModel}); тогда `value` не требуется.
   */
  value?: T | null;
  /**
   * Сигнал значения из {@link FormModel} (M1). Если задан — служит источником истины значения
   * поля (нода не владеет значением, а ссылается на этот сигнал). Имеет приоритет над `value`.
   */
  valueSignal?: Signal<T>;
  /**
   * UI-компонент поля. Опционален: core-часть можно использовать без ссылки на компонент
   * (значение/валидация работают без UI; компонент нужен только для рендеринга).
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  component?: ComponentType<any>;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  componentProps?: any;
  validators?: ValidatorFn<T>[];
  asyncValidators?: AsyncValidatorFn<T>[];
  disabled?: boolean;
  updateOn?: 'change' | 'blur' | 'submit';
  /** Задержка (в мс) перед запуском асинхронной валидации */
  debounce?: number;
}
```

_Source: src/core/types/deep-schema.ts_

### FieldControlState

**Kind:** `interface`

Состояние поля формы, возвращаемое хуком {@link useFormControl} для {@link FieldNode}.

Содержит реактивные данные поля: значение, состояние валидации, флаги взаимодействия
и пользовательские props для компонентов.

**Signature:**
```typescript
export interface FieldControlState<T> {
  /**
   * Текущее значение поля.
   *
   * @example
   * ```tsx
   * const { value } = useFormControl(emailField);
   * console.log(value); // "user@example.com"
   * ```
   */
  value: T;

  /**
   * Флаг асинхронной валидации или загрузки.
   * `true` когда выполняется асинхронный валидатор.
   *
   * @example
   * ```tsx
   * const { pending } = useFormControl(usernameField);
   *
   * return (
   *   <div>
   *     <input {...props} />
   *     {pending && <Spinner size="small" />}
   *   </div>
   * );
   * ```
   */
  pending: boolean;

  /**
   * Флаг отключения поля.
   * `true` когда поле недоступно для редактирования.
   *
   * @example
   * ```tsx
   * const { disabled, value } = useFormControl(field);
   *
   * return (
   *   <input
   *     value={value}
   *     disabled={disabled}
   *     className={disabled ? 'opacity-50' : ''}
   *   />
   * );
   * ```
   */
  disabled: boolean;

  /**
   * Массив ошибок валидации.
   * Пустой массив означает отсутствие ошибок.
   *
   * @example
   * ```tsx
   * const { errors } = useFormControl(field);
   *
   * return (
   *   <ul className="error-list">
   *     {errors.map((error, i) => (
   *       <li key={i}>{error.message}</li>
   *     ))}
   *   </ul>
   * );
   * ```
   */
  errors: ValidationError[];

  /**
   * Флаг валидности поля.
   * `true` когда поле прошло все валидации (errors.length === 0).
   *
   * @example
   * ```tsx
   * const { valid } = useFormControl(field);
   *
   * return (
   *   <input className={valid ? 'border-green' : 'border-gray'} />
   * );
   * ```
   */
  valid: boolean;

  /**
   * Флаг невалидности поля.
   * `true` когда есть ошибки валидации (errors.length > 0).
   * Противоположность {@link valid}.
   *
   * @example
   * ```tsx
   * const { invalid } = useFormControl(field);
   *
   * return (
   *   <input
   *     aria-invalid={invalid}
   *     className={invalid ? 'border-red' : ''}
   *   />
   * );
   * ```
   */
  invalid: boolean;

  /**
   * Флаг взаимодействия с полем.
   * `true` после того как поле потеряло фокус (blur) хотя бы один раз.
   *
   * @example
   * ```tsx
   * const { touched, invalid } = useFormControl(field);
   *
   * // Показываем ошибку только после взаимодействия
   * const showError = touched && invalid;
   * ```
   */
  touched: boolean;

  /**
   * Флаг для отображения ошибки.
   * Комбинация touched && invalid - удобный shortcut для UI.
   *
   * @example
   * ```tsx
   * const { shouldShowError, errors } = useFormControl(field);
   *
   * return (
   *   <div>
   *     <input {...props} />
   *     {shouldShowError && (
   *       <span className="error">{errors[0]?.message}</span>
   *     )}
   *   </div>
   * );
   * ```
   */
  shouldShowError: boolean;

  /**
   * Пользовательские props для передачи в UI-компоненты.
   * Устанавливаются через {@link FieldNode.setComponentProps}.
   *
   * @example
   * ```tsx
   * // Установка props
   * field.setComponentProps({
   *   placeholder: 'Enter email...',
   *   maxLength: 100,
   *   autoComplete: 'email'
   * });
   *
   * // Использование в компоненте
   * const { componentProps, value } = useFormControl(field);
   *
   * return (
   *   <input
   *     value={value}
   *     placeholder={componentProps.placeholder}
   *     maxLength={componentProps.maxLength}
   *     autoComplete={componentProps.autoComplete}
   *   />
   * );
   * ```
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  componentProps: Record<string, any>;
}
```

**Examples:**

Базовое использование
```tsx
interface Props {
control: FieldNode<string>;
}

function TextField({ control }: Props) {
const state = useFormControl(control);

return (
<div>
<input
value={state.value}
disabled={state.disabled}
onChange={e => control.setValue(e.target.value)}
/>
{state.shouldShowError && state.errors[0] && (
<span className="error">{state.errors[0].message}</span>
)}
</div>
);
}
```

**See also:**
- {@link useFormControl} - хук для получения состояния
- {@link ArrayControlState} - состояние для массивов

_Source: src/hooks/types.ts_

### FieldNode

**Kind:** `class`

FieldNode - узел для отдельного поля формы

**Signature:**
```typescript
export class FieldNode<T> extends FormNode<T> {
  // ============================================================================
  // Приватные сигналы
  // ============================================================================ /* … */ }
```

**Examples:**

```typescript
const field = new FieldNode({
  value: '',
  component: Input,
  validators: [required, email],
});

field.setValue('test@mail.com');
await field.validate();
console.log(field.valid.value); // true
```

_Source: src/core/nodes/field-node.ts_

### FieldPathSegment

**Kind:** `type`

Тип для путей к полям (field paths)
Используется в навигации по полям вместо any

**Signature:**
```typescript
export type FieldPathSegment = {
  key: string;
  index?: number;
};
```

_Source: src/core/types/index.ts_

### FieldStatus

**Kind:** `type`

Статус поля формы

**Signature:**
```typescript
export type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
```

_Source: src/core/types/index.ts_

### FormArrayProxy

**Kind:** `type`

Комбинированный тип для ArrayNode с Proxy доступом к элементам

Объединяет методы и свойства ArrayNode с типизированным доступом к элементам массива.

**Signature:**
```typescript
export type FormArrayProxy<T extends object> = ArrayNode<T> & {
  /**
   * Безопасный доступ к элементу массива по индексу
   * Возвращает GroupNode с типизированными полями или undefined
   */
  at(index: number): FormProxy<T> | undefined;

  /**
   * Итерация по элементам массива с типизированными элементами
   */
  forEach(callback: (item: FormProxy<T>, index: number) => void): void;

  /**
   * Маппинг элементов массива с типизированными элементами
   */
  map<R>(callback: (item: FormProxy<T>, index: number) => R): R[];
};
```

**Examples:**

```typescript
interface TodoItem {
  title: string;
  completed: boolean;
}

const todos: FormArrayProxy<TodoItem> = new ArrayNode(schema);

// Доступ к методам ArrayNode
todos.push({ title: 'New todo', completed: false });
todos.removeAt(0);

// Доступ к элементам (через Proxy)
todos.at(0)?.title.setValue('Updated title');

// Итерация
todos.forEach((item, i) => {
  console.log(item.title.value.value);
});
```

_Source: src/core/types/form-proxy.ts_

### FormChangeCallback

**Kind:** `type`

Callback для обработки событий изменения

**Signature:**
```typescript
export type FormChangeCallback = (event: FormChangeEvent) => void;
```

_Source: src/core/utils/form-observer.ts_

### FormChangeEvent

**Kind:** `interface`

Событие изменения в форме

**Signature:**
```typescript
export interface FormChangeEvent {
  /** Тип изменения */
  type: FormChangeType;
  /** Путь к полю */
  path: string;
  /** Timestamp события */
  timestamp: number;
  /** Старое значение */
  oldValue?: unknown;
  /** Новое значение */
  newValue: unknown;
}
```

_Source: src/core/utils/form-observer.ts_

### FormChangeType

**Kind:** `type`

Тип события изменения в форме

**Signature:**
```typescript
export type FormChangeType = 'value' | 'status' | 'errors' | 'touched' | 'dirty';
```

_Source: src/core/utils/form-observer.ts_

### FormControlsProxy

**Kind:** `type`

Мапит тип модели данных T на правильные типы узлов формы

Рекурсивно определяет типы узлов на основе структуры данных:
- `T[K] extends Array<infer U>` где U - объект → `FormArrayProxy<U>`
- `T[K] extends Array<infer U>` где U - примитив → `FieldNode<T[K]>` (массив как обычное поле)
- `T[K] extends object` → `FormProxy<T[K]>` (вложенная форма с типизацией)
- `T[K]` примитив → `FieldNode<T[K]>` (простое поле)

Использует NonNullable для правильной обработки опциональных полей

**Signature:**
```typescript
export type FormControlsProxy<T> = {
  // `-?` снимает опциональность: для каждого поля схемы прокси всегда содержит узел
  // (включая опциональные поля — у них узел существует, опционально лишь значение).
  // Без этого `control.optionalField` имел бы тип `FieldNode<...> | undefined`.
  [K in keyof T]-?: NonNullable<T[K]> extends ReadonlyArray<infer U>
    ? IsGroupObject<U> extends true
      ? FormArrayProxy<U & object> // Массив объектов → FormArrayProxy
      : FieldNode<T[K]> // Массив примитивов → FieldNode
    : IsGroupObject<NonNullable<T[K]>> extends true
      ? FormProxy<NonNullable<T[K]>> // Обычный объект → FormProxy (рекурсивно!)
      : FieldNode<T[K]>; // Примитивы и спец-объекты (Date/File/Blob) → FieldNode
};
```

_Source: src/core/types/form-proxy.ts_

### FormErrorHandler

**Kind:** `class`

Централизованный обработчик ошибок для форм

Обеспечивает:
- Единообразное логирование ошибок в DEV режиме
- Гибкие стратегии обработки (throw/log/convert)
- Типобезопасное извлечение сообщений из Error/string/unknown

**Signature:**
```typescript
export class FormErrorHandler {
  /**
   * Обработать ошибку согласно заданной стратегии
   *
   * @param error Ошибка для обработки (Error | string | unknown)
   * @param context Контекст ошибки для логирования (например, 'AsyncValidator', 'BehaviorRegistry')
   * @param strategy Стратегия обработки (THROW | LOG | CONVERT)
   * @returns ValidationError если strategy = CONVERT, undefined если strategy = LOG, никогда не возвращается если strategy = THROW
   *
   * @example
   * ```typescript
   * // THROW - пробросить ошибку
   * try {
   *   riskyOperation();
   * } catch (error) {
   *   FormErrorHandler.handle(error, 'RiskyOperation', ErrorStrategy.THROW);
   *   // Этот код никогда не выполнится
   * }
   *
   * // LOG - залогировать и продолжить
   * try {
   *   nonCriticalOperation();
   * } catch (error) {
   *   FormErrorHandler.handle(error, 'NonCritical', ErrorStrategy.LOG);
   *   // Продолжаем выполнение
   * }
   *
   * // CONVERT - конвертировать в ValidationError
   * try {
   *   await validator(value);
   * } catch (error) {
   *   const validationError = FormErrorHandler.handle(
   *     error,
   *     'AsyncValidator',
   *     ErrorStrategy.CONVERT
   *   );
   *   return validationError;
   * }
   * ```
   */ /* … */ }
```

**Examples:**

```typescript
// В async validator (конвертировать в ValidationError)
try {
  await validateEmail(value);
} catch (error) {
  return FormErrorHandler.handle(error, 'EmailValidator', ErrorStrategy.CONVERT);
}

// В behavior applicator (пробросить критичную ошибку)
try {
  applyBehavior(schema);
} catch (error) {
  FormErrorHandler.handle(error, 'BehaviorApplicator', ErrorStrategy.THROW);
}

// В validator (залогировать и продолжить)
try {
  validator(value);
} catch (error) {
  FormErrorHandler.handle(error, 'Validator', ErrorStrategy.LOG);
}
```

_Source: src/core/utils/error-handler.ts_

### FormFields

**Kind:** `type`

Тип для конфига с полями (FormSchema generic constraint)
Используется вместо `Record<string, any>` для схем форм

**Signature:**
```typescript
export type FormFields = Record<string, FormValue>;
```

**Deprecated:** (no message)

_Source: src/core/types/index.ts_

### FormModel

**Kind:** `type`

FormModel — value-proxy объекта `T` + {@link ModelApi}.

**Signature:**
```typescript
export type FormModel<T> = ModelObject<T> & ModelApi<T>;
```

_Source: src/core/model/types.ts_

### FormNode

**Kind:** `class`

Абстрактный базовый класс для всех узлов формы.

Все узлы (поля, группы, массивы) наследуют от этого класса и реализуют
единый интерфейс для работы с состоянием и валидацией.

Template Method паттерн используется для управления состоянием:
общие signals (`_touched`, `_dirty`, `_status`) живут в базовом классе,
publi-методы (`markAsTouched`, `disable`, …) реализованы здесь, а protected
hooks (`onMarkAsTouched`, `onDisable`, …) переопределяются в наследниках.

**Signature:**
```typescript
export abstract class FormNode<T> {
  // ============================================================================
  // Protected состояние (для Template Method паттерна)
  // ============================================================================

  /**
   * Пользователь взаимодействовал с узлом (touched)
   * Protected: наследники могут читать/изменять через методы
   */ /* … */ }
```

**Examples:**

```typescript
// FormNode не используется напрямую — экземпляры приходят из getReformerForm.
import { getReformerForm, FormNode } from '@reformer/core';

const form = getReformerForm({ email: '' });
form.email instanceof FormNode; // true
form.email.markAsTouched();
```

_Source: src/core/nodes/form-node.ts_

### FormObserver

**Kind:** `class`

FormObserver - мониторинг изменений в форме

Полезен для:
- Отладки сложных форм
- Логирования изменений для аудита
- Синхронизации с внешними системами

**Signature:**
```typescript
export class FormObserver<T extends FormFields> { /* … */ }
```

**Examples:**

```typescript
const observer = new FormObserver(form, {
  enableLogging: true,
  eventTypes: ['value', 'errors']
});

// Подписка на события
const unsubscribe = observer.subscribe((event) => {
  console.log(`${event.type} at ${event.path}:`, event.newValue);
});

// Включить трассировку
const disposeTracing = observer.enableTracing();

// Cleanup
unsubscribe();
disposeTracing();
```

_Source: src/core/utils/form-observer.ts_

### FormObserverOptions

**Kind:** `interface`

Опции для FormObserver

**Signature:**
```typescript
export interface FormObserverOptions {
  /** Включить логирование в консоль (по умолчанию true в DEV) */
  enableLogging?: boolean;
  /** Фильтр типов событий */
  eventTypes?: FormChangeType[];
  /** Фильтр путей полей (regex или массив) */
  pathFilter?: RegExp | string[];
}
```

_Source: src/core/utils/form-observer.ts_

### FormProxy

**Kind:** `type`

Комбинированный тип для GroupNode с Proxy доступом к полям

Объединяет методы и свойства GroupNode с типизированными полями формы.
Это позволяет использовать как API GroupNode, так и прямой доступ к полям.

**Signature:**
```typescript
export type FormProxy<T> = GroupNode<T> & FormControlsProxy<T>;
```

**Examples:**

```typescript
interface UserForm {
  email: string;
  profile: {
    name: string;
    age: number;
  };
}

const form = createForm<UserForm>(schema);

// Доступ к методам GroupNode
await form.validate();
const values = form.getValue();
console.log(form.valid.value);

// Прямой доступ к полям (через Proxy)
form.email.setValue('test@mail.com');
form.profile.name.setValue('John');
```

_Source: src/core/types/form-proxy.ts_

### FormSchema

**Kind:** `type`

Автоматически определяет тип схемы на основе TypeScript типа:
- `T[] -> [FormSchema<T>]` (массив с одним элементом)
- `object -> FormSchema<T>` (группа)
- `primitive -> FieldConfig<T>` (поле)

Использует NonNullable для корректной обработки опциональных полей

**Signature:**
```typescript
export type FormSchema<T> = {
  [K in keyof T]: NonNullable<T[K]> extends string | number | boolean
    ? FieldConfig<T[K]>
    : NonNullable<T[K]> extends Array<infer U>
      ? U extends string | number | boolean
        ? FieldConfig<T[K]>
        : U extends Date | File | Blob | AnyFunction
          ? FieldConfig<T[K]>
          : [FormSchema<U>]
      : NonNullable<T[K]> extends Date | File | Blob | AnyFunction
        ? FieldConfig<T[K]>
        : FormSchema<NonNullable<T[K]>>;
};
```

**Examples:**

```typescript
interface Form {
  name: string;                    // → FieldConfig<string>
  address: {                       // → FormSchema<Address>
    city: string;
    street: string;
  };
  items?: Array<{                  // → [FormSchema<Item>] (опциональный)
    title: string;
    price: number;
  }>;
}

const schema: FormSchema<Form> = {
  name: { value: '', component: Input },
  address: {
    city: { value: '', component: Input },
    street: { value: '', component: Input },
  },
  items: [{
    title: { value: '', component: Input },
    price: { value: 0, component: Input },
  }],
};
```

_Source: src/core/types/deep-schema.ts_

### FormStatusMachine

**Kind:** `class`

FormStatusMachine - управляет состоянием поля формы

Предоставляет:
- Единый источник истины для статуса
- Computed signals для derived состояний (valid, invalid, pending, disabled)
- Валидацию переходов между состояниями

**Signature:**
```typescript
export class FormStatusMachine {
  /** Внутренний сигнал статуса */ /* … */ }
```

**Examples:**

```typescript
const statusMachine = new FormStatusMachine('valid');

// Начало валидации
statusMachine.startValidation();
console.log(statusMachine.pending.value); // true

// Завершение валидации с ошибками
statusMachine.completeValidation(true);
console.log(statusMachine.invalid.value); // true

// Отключение поля
statusMachine.disable();
console.log(statusMachine.disabled.value); // true
```

_Source: src/core/utils/status-machine.ts_

### FormSubmitter

**Kind:** `class`

FormSubmitter - управляет процессом отправки формы

**Signature:**
```typescript
export class FormSubmitter<T extends FormFields> {
  /** Внутренний сигнал состояния отправки */ /* … */ }
```

**Examples:**

```typescript
const submitter = new FormSubmitter(form);

// Простой submit
const result = await submitter.submit(async (values) => {
  return await api.saveForm(values);
});

// Проверка состояния
if (submitter.submitting.value) {
  console.log('Форма отправляется...');
}
```

_Source: src/core/utils/form-submitter.ts_

### FormValue

**Kind:** `type`

Represents any valid form value type
Use this instead of 'any' for form values to maintain type safety

**Signature:**
```typescript
export type FormValue =
  | string
  | number
  | boolean
  | null
  | undefined
  | Date
  | File
  | FormValue[]
  | { [key: string]: FormValue };
```

_Source: src/core/types/index.ts_

### futureDate

**Kind:** `function`

Фабрика валидатора, проверяющего что дата не в прошлом.

Пустые и невалидные даты пропускаются.

**Signature:**
```typescript
export function futureDate<
  TForm = unknown,
  TField extends string | Date | undefined = string | Date,
>(options?: ValidateOptions): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.eventDate, futureDate());
validate(path.appointmentDate, futureDate({ message: 'Дата записи должна быть в будущем' }));
```

_Source: src/core/validation/validators/future-date.ts_

### getNodeForSignal

**Kind:** `function`

Получить FieldNode по сигналу модели (или undefined, если форма не построена/поле не материализовано,
например элемент массива в родительской форме).

**Signature:**
```typescript
export function getNodeForSignal(signal: Signal<any>): FormNode<any> | undefined
```

_Source: src/core/utils/signal-node-registry.ts_

### getNodeType

**Kind:** `function`

Получить тип узла как строку (для отладки)

Полезно для логирования и отладки

**Signature:**
```typescript
export function getNodeType(node: unknown): string
```

**Parameters:**
- `node` — - Узел для проверки

**Returns:** Строковое название типа узла

**Examples:**

```typescript
console.log('Node type:', getNodeType(node)); // "FieldNode" | "GroupNode" | "ArrayNode" | "FormNode" | "Unknown"
```

_Source: src/core/utils/type-guards.ts_

### GroupNode

**Kind:** `class`

GroupNode - узел для группы полей

Создаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`
(M1: `createForm({ model, schema })`); валидация/behavior живут на слое модели
(`validateFormModel`, `computeFrom`/`enableWhen`/…), а не на ноде.

**Signature:**
```typescript
export class GroupNode<T> extends FormNode<T> {
  // ============================================================================
  // Приватные поля
  // ============================================================================ /* … */ }
```

**Examples:**

```typescript
const form = new GroupNode({
  email: { valueSignal: model.$.email, component: Input },
  password: { valueSignal: model.$.password, component: Input },
});

// Прямой доступ к полям через Proxy
const proxy = form.getProxy();
proxy.email.setValue('test@mail.com');
await proxy.validate();
console.log(proxy.valid.value);
```

_Source: src/core/nodes/group-node.ts_

### GroupNodeConfig

**Kind:** `interface`

Конфигурация GroupNode.

Под M1 группа создаётся из плоской {@link FormSchema} (дерево field-конфигов). Обёртка
`{ form }` сохранена для совместимости вызова, legacy behavior/validation-схемы удалены (Ф7).

**Signature:**
```typescript
export interface GroupNodeConfig<T> {
  /** Схема структуры формы (поля и их конфигурация) */
  form: FormSchema<T>;
}
```

_Source: src/core/types/index.ts_

### integer

**Kind:** `function`

Фабрика валидатора, проверяющего что число — целое.

Пустые значения и не-числа пропускаются (используйте `required` и `isNumber`).

**Signature:**
```typescript
export function integer<TForm = unknown, TField extends number | undefined = number>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.age, integer());
validate(path.count, integer({ message: 'Должно быть целым числом' }));
```

_Source: src/core/validation/validators/integer.ts_

### isArrayNode

**Kind:** `function`

Проверить, является ли значение ArrayNode (массив форм)

ArrayNode представляет массив вложенных форм (обычно GroupNode)
и имеет array-like методы (push, removeAt, at)

**Signature:**
```typescript
export function isArrayNode(value: unknown): value is ArrayNode<FormFields>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является ArrayNode

**Examples:**

```typescript
if (isArrayNode(node)) {
  node.push(); //  OK - добавить элемент
  node.removeAt(0); //  OK - удалить элемент
  const item = node.at(0); //  OK - получить элемент
}
```

_Source: src/core/utils/type-guards.ts_

### isDate

**Kind:** `function`

Фабрика валидатора, проверяющего что значение — валидная дата.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function isDate<TForm = unknown, TField extends string | Date | undefined = string | Date>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.birthDate, isDate());
validate(path.eventDate, isDate({ message: 'Введите корректную дату' }));
```

_Source: src/core/validation/validators/is-date.ts_

### isDerived

**Kind:** `function`

Производный ли сигнал (читают bulk-сеттеры, чтобы не затирать вычисляемые поля).

**Signature:**
```typescript
export function isDerived(signal: Signal<any>): boolean
```

_Source: src/core/utils/derived-registry.ts_

### isFieldNode

**Kind:** `function`

Проверить, является ли значение FieldNode (примитивное поле)

FieldNode представляет примитивное поле формы (string, number, boolean и т.д.)
и имеет валидаторы, но не имеет вложенных полей или элементов массива

**Signature:**
```typescript
export function isFieldNode(value: unknown): value is FieldNode<FormValue>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является FieldNode

**Examples:**

```typescript
if (isFieldNode(node)) {
  node.validators; //  OK
  node.asyncValidators; //  OK
  node.markAsTouched(); //  OK
}
```

_Source: src/core/utils/type-guards.ts_

### isFormNode

**Kind:** `function`

Проверить, является ли значение любым FormNode

Проверяет базовые свойства, общие для всех типов узлов

**Signature:**
```typescript
export function isFormNode(value: unknown): value is FormNode<FormValue>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является FormNode

**Examples:**

```typescript
if (isFormNode(value)) {
  value.setValue(newValue);
  value.validate();
}
```

_Source: src/core/utils/type-guards.ts_

### isGroupNode

**Kind:** `function`

Проверить, является ли значение GroupNode (объект с вложенными полями)

GroupNode представляет объект с вложенными полями формы: имеет навигацию по полям
(`getFieldByPath`/`fields`) и НЕ имеет array-методов (`items`/`push`/`removeAt`).

**Signature:**
```typescript
export function isGroupNode(value: unknown): value is GroupNode<FormFields>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является GroupNode

**Examples:**

```typescript
if (isGroupNode(node)) {
  node.getFieldByPath('user.email'); //  OK
}
```

_Source: src/core/utils/type-guards.ts_

### isNumber

**Kind:** `function`

Фабрика валидатора, проверяющего что значение — конечное число (не NaN, не строка).

Пустые значения (`null`/`undefined`) пропускаются (используйте `required` для обязательности).
В отличие от других number-валидаторов, **не** пропускает не-числа — это его задача.

**Signature:**
```typescript
export function isNumber<TForm = unknown, TField extends number | undefined = number>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.amount, isNumber());
validate(path.amount, isNumber({ message: 'Введите число' }));
```

_Source: src/core/validation/validators/is-number.ts_

### markDerived

**Kind:** `function`

Пометить сигнал производным (вызывают compute/computeFrom).

**Signature:**
```typescript
export function markDerived(signal: Signal<any>): void
```

_Source: src/core/utils/derived-registry.ts_

### max

**Kind:** `function`

Фабрика валидатора максимального числового значения.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function max<TForm = unknown, TField extends number | undefined = number>(
  maxValue: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
import { validate } from '@reformer/core';
import { max } from '@reformer/core/validators';

validate(path.quantity, max(100));
validate(path.discount, max(50, { message: 'Не более 50%' }));
```

_Source: src/core/validation/validators/max.ts_

### maxAge

**Kind:** `function`

Фабрика валидатора максимального возраста (по дате рождения).

Пустые и невалидные даты пропускаются.

**Signature:**
```typescript
export function maxAge<
  TForm = unknown,
  TField extends string | Date | null | undefined = string | Date,
>(maxAgeValue: number, options?: ValidateOptions): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.birthDate, maxAge(65));
validate(path.birthDate, maxAge(100, { message: 'Проверьте дату рождения' }));
```

_Source: src/core/validation/validators/max-age.ts_

### maxDate

**Kind:** `function`

Фабрика валидатора максимальной даты (включительно).

Пустые и невалидные даты пропускаются.

**Signature:**
```typescript
export function maxDate<
  TForm = unknown,
  TField extends string | Date | null | undefined = string | Date,
>(maxDateValue: Date, options?: ValidateOptions): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.birthDate, maxDate(new Date())); // не позже сегодня
validate(path.endDate, maxDate(new Date('2025-12-31')));
```

_Source: src/core/validation/validators/max-date.ts_

### maxLength

**Kind:** `function`

Фабрика валидатора максимальной длины строки или массива.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function maxLength<TForm = unknown, TField = unknown>(
  maxLen: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.name, maxLength(50));
validate(path.bio, maxLength(500, { message: 'Max 500 символов' }));
```

_Source: src/core/validation/validators/max-length.ts_

### min

**Kind:** `function`

Фабрика валидатора минимального числового значения.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function min<TForm = unknown, TField extends number | undefined = number>(
  minValue: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
import { validate } from '@reformer/core';
import { min } from '@reformer/core/validators';

validate(path.age, min(18));
validate(path.quantity, min(1, { message: 'Минимум 1' }));
```

_Source: src/core/validation/validators/min.ts_

### minAge

**Kind:** `function`

Фабрика валидатора минимального возраста (по дате рождения).

Пустые и невалидные даты пропускаются.

**Signature:**
```typescript
export function minAge<
  TForm = unknown,
  TField extends string | Date | null | undefined = string | Date,
>(minAgeValue: number, options?: ValidateOptions): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.birthDate, minAge(18));
validate(path.birthDate, minAge(21, { message: 'Вам должно быть не менее 21 года' }));
```

_Source: src/core/validation/validators/min-age.ts_

### minDate

**Kind:** `function`

Фабрика валидатора минимальной даты (включительно).

Пустые и невалидные даты пропускаются (используйте `required` и `isDate`).

**Signature:**
```typescript
export function minDate<
  TForm = unknown,
  TField extends string | Date | null | undefined = string | Date,
>(minDateValue: Date, options?: ValidateOptions): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.eventDate, minDate(new Date('2024-01-01')));
validate(path.startDate, minDate(new Date(), { message: 'Дата должна быть не раньше сегодня' }));
```

_Source: src/core/validation/validators/min-date.ts_

### minLength

**Kind:** `function`

Фабрика валидатора минимальной длины строки или массива.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function minLength<TForm = unknown, TField = unknown>(
  minLen: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.name, minLength(2));
validate(path.password, minLength(8, { message: 'Min 8 символов' }));
```

_Source: src/core/validation/validators/min-length.ts_

### ModelApi

**Kind:** `interface`

API уровня модели (доступно на корне и под-моделях элементов массива).

⚠️ Имена методов (`$`/`get`/`set`/`patch`/`isDirty`/`reset`/`signalAt`/`captureInitial`)
зарезервированы: одноимённое поле формы их затеняет (редкий краевой случай).

**Signature:**
```typescript
export interface ModelApi<T> {
  /** Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`. */
  readonly $: ModelSignals<T>;
  /** Снимок значений (без подписки) — для submit. */
  get(): T;
  /** Массовая замена значений (load с сервера). Не меняет initial-снимок. */
  set(value: Partial<T>): void;
  /** Частичное слияние значений. */
  patch(value: Partial<T>): void;
  /** Отличаются ли текущие значения от initial-снимка (value-diff). */
  isDirty(): boolean;
  /** Сбросить значения к initial-снимку. */
  reset(): void;
  /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */
  captureInitial(): void;
  /** Резолв строкового пути в сигнал (для error-routing/мостов). */
  signalAt(path: string): PathAwareSignal<unknown> | undefined;
}
```

_Source: src/core/model/types.ts_

### ModelArray

**Kind:** `interface`

Реактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;
`map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.

**Signature:**
```typescript
export interface ModelArray<U> {
  /** Реактивная длина. */
  readonly length: number;
  /** Добавить элемент в конец (значение элемента целиком). */
  push(item: U): void;
  /** Вставить элемент по индексу. */
  insertAt(index: number, item: U): void;
  /** Удалить элемент по индексу. */
  removeAt(index: number): void;
  /** Переместить элемент. */
  move(from: number, to: number): void;
  /** Поменять местами два элемента. */
  swap(a: number, b: number): void;
  /** Очистить массив. */
  clear(): void;
  /** Под-модель элемента по индексу (для объектных элементов) или значение (для примитивных). */
  at(index: number): U extends object ? FormModel<U> : U | undefined;
  /** Map по элементам: объектные → {@link FormModel}, примитивные → значение. */
  map<R>(fn: (item: U extends object ? FormModel<U> : U, index: number) => R): R[];
  /** Итерация по элементам. */
  forEach(fn: (item: U extends object ? FormModel<U> : U, index: number) => void): void;
  /** Снимок массива значений (без подписки). */
  toArray(): U[];
  /** Индексный value-доступ. */
  [index: number]: ModelValue<U>;
}
```

_Source: src/core/model/types.ts_

### ModelArrayControl

**Kind:** `interface`

Минимальный контракт реактивного массива модели, используемый узлом.

**Signature:**
```typescript
export interface ModelArrayControl<TItem extends object> {
  readonly length: number;
  at(index: number): FormModel<TItem> | undefined;
  push(item: TItem): void;
  insertAt(index: number, item: TItem): void;
  removeAt(index: number): void;
  move(from: number, to: number): void;
  swap(a: number, b: number): void;
  clear(): void;
  toArray(): TItem[];
}
```

_Source: src/core/nodes/model-array-node.ts_

### ModelArrayNode

**Kind:** `class`

**Signature:**
```typescript
export class ModelArrayNode<T extends object> extends FormNode<T[]> { /* … */ }
```

_Source: src/core/nodes/model-array-node.ts_

### ModelObject

**Kind:** `type`

value-proxy объекта: поля доступны как обычные свойства (чтение реактивно
внутри `effect`/`computed`, запись — присваиванием).

**Signature:**
```typescript
export type ModelObject<T> = {
  [K in keyof T]: ModelValue<T[K]>;
};
```

_Source: src/core/model/types.ts_

### ModelSignals

**Kind:** `type`

Дерево сигналов (escape-hatch `model.$`): листья → {@link PathAwareSignal},
объекты → вложенное дерево, массивы → индексируемое дерево под-сигналов.

**Signature:**
```typescript
export type ModelSignals<T> = {
  [K in keyof T]: NonNullable<T[K]> extends ReadonlyArray<infer U>
    ? { readonly length: number; readonly [index: number]: ModelSignals<U & object> }
    : NonNullable<T[K]> extends Opaque
      ? PathAwareSignal<T[K]>
      : NonNullable<T[K]> extends object
        ? ModelSignals<NonNullable<T[K]>>
        : PathAwareSignal<T[K]>;
};
```

_Source: src/core/model/types.ts_

### ModelValidationResult

**Kind:** `interface`

Результат валидации: ошибки по пути поля.

**Signature:**
```typescript
export interface ModelValidationResult {
  valid: boolean;
  /** Ключ — путь поля (`'coBorrowers.0.relationship'`). Только поля с ошибками. */
  errors: Record<string, ValidationError[]>;
}
```

_Source: src/core/model/validate-model.ts_

### ModelValidator

**Kind:** `type`

Валидатор слоя данных. `value` — значение поля; `model` — ближайший scope (под-модель элемента
массива или корень); `root` — корневая модель.

**Signature:**
```typescript
export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
  value: TValue,
  model: TModel,
  root: TRoot
) => ValidationError | null | Promise<ValidationError | null>;
```

_Source: src/core/model/validate-model.ts_

### ModelValue

**Kind:** `type`

Значение поля в value-доступе модели:
- массив → {@link ModelArray}
- спец-объект (Date/File/Blob) → как есть
- объект → вложенный value-proxy
- примитив → значение

**Signature:**
```typescript
export type ModelValue<V> =
  NonNullable<V> extends ReadonlyArray<infer U>
    ? ModelArray<U>
    : NonNullable<V> extends Opaque
      ? V
      : NonNullable<V> extends object
        ? ModelObject<NonNullable<V>>
        : V;
```

_Source: src/core/model/types.ts_

### multipleOf

**Kind:** `function`

Фабрика валидатора, проверяющего что число кратно заданному.

Пустые значения и не-числа пропускаются (используйте `required` и `isNumber`).

**Signature:**
```typescript
export function multipleOf<TForm = unknown, TField extends number | undefined = number>(
  divisor: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.price, multipleOf(0.01));
validate(path.rating, multipleOf(0.5, { message: 'Только шаг 0.5' }));
```

_Source: src/core/validation/validators/multiple-of.ts_

### NodeFactory

**Kind:** `class`

Фабрика для создания узлов формы.

Определяет тип конфига и создаёт соответствующий узел (FieldNode, GroupNode, ArrayNode).
Используется внутри `getReformerForm`/`group`/`array` — явно вызывать обычно не нужно.

**Signature:**
```typescript
export class NodeFactory {
  /**
   * Создает узел формы на основе конфигурации
   *
   * ✅ ОБНОВЛЕНО: Теперь поддерживает массивы напрямую
   *
   * Автоматически определяет тип узла:
   * - FieldNode: имеет value и component
   * - ArrayNode: массив [schema, ...items] или { schema, initialItems }
   * - GroupNode: объект без value, component, schema
   *
   * @param config Конфигурация узла
   * @returns Экземпляр FieldNode, GroupNode или ArrayNode
   * @throws Error если конфиг не соответствует ни одному типу
   *
   * @example
   * ```typescript
   * const factory = new NodeFactory();
   *
   * // FieldNode
   * const field = factory.createNode({
   *   value: 'test@mail.com',
   *   component: Input,
   *   validators: [required, email]
   * });
   *
   * // GroupNode
   * const group = factory.createNode({
   *   email: { value: '', component: Input },
   *   password: { value: '', component: Input }
   * });
   *
   * // ArrayNode (объект)
   * const array = factory.createNode({
   *   schema: { title: { value: '', component: Input } },
   *   initialItems: [{ title: 'Item 1' }]
   * });
   *
   * // ArrayNode (массив) - новый формат
   * const array2 = factory.createNode([
   *   { title: { value: '', component: Input } }, // schema
   *   { title: 'Item 1' }, // initial item 1
   *   { title: 'Item 2' }  // initial item 2
   * ]);
   * ```
   */ /* … */ }
```

**Examples:**

```typescript
import { NodeFactory } from '@reformer/core';

const node = NodeFactory.create({ value: '' }); // → FieldNode<string>
```

_Source: src/core/factories/node-factory.ts_

### nonNegative

**Kind:** `function`

Фабрика валидатора, проверяющего что число неотрицательное (`≥ 0`).

Пустые значения и не-числа пропускаются (используйте `required` и `isNumber`).

**Signature:**
```typescript
export function nonNegative<TForm = unknown, TField extends number | undefined = number>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.quantity, nonNegative());
validate(path.balance, nonNegative({ message: 'Баланс не может быть отрицательным' }));
```

_Source: src/core/validation/validators/non-negative.ts_

### nonZero

**Kind:** `function`

Фабрика валидатора, проверяющего что число не равно нулю.

Пустые значения и не-числа пропускаются (используйте `required` и `isNumber`).

**Signature:**
```typescript
export function nonZero<TForm = unknown, TField extends number | undefined = number>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.quantity, nonZero());
validate(path.divisor, nonZero({ message: 'Не может быть нулём' }));
```

_Source: src/core/validation/validators/non-zero.ts_

### ObservableForm

**Kind:** `interface`

Интерфейс формы для observer

**Signature:**
```typescript
export interface ObservableForm<T extends FormFields> {
  value: { value: T };
  status: { value: FieldStatus };
  errors: { value: ValidationError[] };
  touched: { value: boolean };
  dirty: { value: boolean };
  getFieldByPath(path: string): ObservableFormNode | undefined;
}
```

_Source: src/core/utils/form-observer.ts_

### ObservableFormNode

**Kind:** `interface`

Интерфейс узла формы для observer

**Signature:**
```typescript
export interface ObservableFormNode {
  value: { value: FormValue };
  status: { value: FieldStatus };
  errors: { value: ValidationError[] };
  touched: { value: boolean };
  dirty: { value: boolean };
}
```

_Source: src/core/utils/form-observer.ts_

### pastDate

**Kind:** `function`

Фабрика валидатора, проверяющего что дата не в будущем.

Пустые и невалидные даты пропускаются.

**Signature:**
```typescript
export function pastDate<TForm = unknown, TField extends string | Date | undefined = string | Date>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.birthDate, pastDate({ message: 'Дата рождения не может быть в будущем' }));
```

_Source: src/core/validation/validators/past-date.ts_

### PathAwareSignal

**Kind:** `type`

Сигнал, который знает свой путь в модели (`'personalData.lastName'`).
Используется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.

**Signature:**
```typescript
export type PathAwareSignal<T> = Signal<T> & {
  /** Путь поля в модели (dot-нотация). На элементах массива включает индекс. */
  readonly __path: string;
};
```

_Source: src/core/model/types.ts_

### pattern

**Kind:** `function`

Фабрика валидатора регулярного выражения.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function pattern<TForm = unknown, TField extends string | undefined = string>(
  regex: RegExp,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.name, pattern(/^[а-яА-Яa-zA-Z]+$/));
validate(path.code, pattern(/^\d+$/, { message: 'Только цифры' }));
```

_Source: src/core/validation/validators/pattern.ts_

### phone

**Kind:** `function`

Фабрика валидатора номера телефона.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function phone<TForm = unknown, TField extends string | undefined = string>(
  options?: PhoneValidatorOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.phone, phone());
validate(path.phone, phone({ format: 'ru' }));
```

_Source: src/core/validation/validators/phone.ts_

### PhoneFormat

**Kind:** `type`

**Signature:**
```typescript
export type PhoneFormat = 'international' | 'ru' | 'us' | 'any';
```

_Source: src/core/validation/validators/phone.ts_

### registerSignalNode

**Kind:** `function`

Связать сигнал модели с его FieldNode.

**Signature:**
```typescript
export function registerSignalNode(signal: Signal<any>, node: FormNode<any>): void
```

_Source: src/core/utils/signal-node-registry.ts_

### RegistryStack

**Kind:** `class`

Generic Registry Stack - утилита для управления стеком регистрации

Используется ValidationRegistry и BehaviorRegistry для tracking активного контекста.
Устраняет дублирование кода между параллельными системами.

**Signature:**
```typescript
export class RegistryStack<T> { /* … */ }
```

**Examples:**

```typescript
class ValidationRegistry {
  private static registryStack = new RegistryStack<ValidationRegistry>();

  static getCurrent() {
    return ValidationRegistry.registryStack.getCurrent();
  }

  beginRegistration() {
    ValidationRegistry.registryStack.push(this);
  }

  endRegistration() {
    ValidationRegistry.registryStack.verify(this, 'ValidationRegistry');
  }
}
```

_Source: src/core/utils/registry-stack.ts_

### required

**Kind:** `function`

Фабрика валидатора обязательного поля.

Возвращает чистую функцию-валидатор `(value, control, root)`. Передаётся в `validate()`.

Пустыми считаются: `null`, `undefined`, `''` (пустая строка).
Для boolean полей требуется значение `true`.

**Signature:**
```typescript
export function required<TForm = unknown, TField = unknown>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
import { validate } from '@reformer/core';
import { required } from '@reformer/core/validators';

validate(path.email, required());
validate(path.phone, required({ message: 'Укажите номер телефона' }));
validate(path.agreeToTerms, required({ message: 'Необходимо принять условия' }));
```

_Source: src/core/validation/validators/required.ts_

### resetWhen

**Kind:** `function`

Сброс значения поля к `resetValue` (по умолчанию `null`), когда `condition` истинно.

**Signature:**
```typescript
export function resetWhen<T>(
  target: Signal<T>,
  condition: () => boolean,
  options?: { resetValue?: T }
): BehaviorCleanup
```

**Examples:**

```typescript
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
```

_Source: src/core/model/behaviors.ts_

### ResourceLoadResult

**Kind:** `type`

Тип для результатов загрузки ресурсов

**Signature:**
```typescript
export type ResourceLoadResult = unknown;
```

_Source: src/core/types/index.ts_

### revalidateWhen

**Kind:** `function`

Вызывает `revalidate()` при изменении зависимостей (не на инициализации). Под M1 валидация
on-demand (`validateFormModel`), поэтому ревалидация выражается явным колбэком.

**Signature:**
```typescript
export function revalidateWhen(
  deps: ReadonlySignal<unknown>[],
  revalidate: () => void
): BehaviorCleanup
```

**Examples:**

```typescript
revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
```

_Source: src/core/model/behaviors.ts_

### runOutsideEffect

**Kind:** `function`

Выполняет функцию вне контекста effect

Более простой вариант для одиночного вызова

**Signature:**
```typescript
export function runOutsideEffect(fn: () => void | Promise<void>): void
```

**Parameters:**
- `fn` — - Функция для выполнения

**Examples:**

```typescript
effect(() => {
  const value = signal.value;
  runOutsideEffect(() => {
    otherSignal.value = transform(value);
  });
});
```

_Source: src/core/utils/safe-effect.ts_

### safeCallback

**Kind:** `function`

Создает callback, который выполняется вне контекста effect

Использует queueMicrotask для отложенного выполнения,
что позволяет модифицировать сигналы без вызова "Cycle detected"

**Signature:**
```typescript
export function safeCallback<TArgs extends unknown[]>(
  callback: (...args: TArgs) => void | Promise<void>
): (...args: TArgs) => void
```

**Parameters:**
- `callback` — - Функция для выполнения

**Returns:** Обёрнутая функция, безопасная для вызова внутри effect

**Examples:**

```typescript
// Вместо:
effect(() => {
  queueMicrotask(() => {
    callback(value, context);
  });
});

// Используем:
effect(() => {
  safeCallback(callback)(value, context);
});
```

_Source: src/core/utils/safe-effect.ts_

### safeDebouncedCallback

**Kind:** `function`

Создает версию callback с поддержкой debounce, безопасную для effect

Комбинирует debounce логику с выходом из effect контекста

**Signature:**
```typescript
export function safeDebouncedCallback(
  callback: () => void | Promise<void>,
  withDebounce: (fn: () => void) => void
): () => void
```

**Parameters:**
- `callback` — - Функция для выполнения
- `withDebounce` — - Функция debounce обёртки из BehaviorRegistry

**Returns:** Обёрнутая функция

**Examples:**

```typescript
return effect(() => {
  const value = node.value.value;
  safeDebouncedCallback(
    () => callback(value, context),
    withDebounce
  )();
});
```

_Source: src/core/utils/safe-effect.ts_

### SetValueOptions

**Kind:** `interface`

Опции для setValue

**Signature:**
```typescript
export interface SetValueOptions {
  /** Не вызывать событие изменения (не триггерить валидацию) */
  emitEvent?: boolean;
  /** Обновить только этот узел, не распространять на родителей */
  onlySelf?: boolean;
}
```

_Source: src/core/nodes/form-node.ts_

### StatusEvent

**Kind:** `type`

События для State Machine

**Signature:**
```typescript
export type StatusEvent =
  | { type: 'START_VALIDATION' }
  | { type: 'VALIDATION_SUCCESS' }
  | { type: 'VALIDATION_FAILURE' }
  | { type: 'DISABLE' }
  | { type: 'ENABLE'; hasErrors?: boolean }
  | { type: 'SET_ERRORS'; hasErrors: boolean };
```

_Source: src/core/utils/status-machine.ts_

### SubmitOptions

**Kind:** `interface`

Опции для submit

**Signature:**
```typescript
export interface SubmitOptions {
  /** Пропустить валидацию перед submit */
  skipValidation?: boolean;
  /** Пропустить markAsTouched перед submit */
  skipTouch?: boolean;
}
```

_Source: src/core/utils/form-submitter.ts_

### SubmitResult

**Kind:** `interface`

Результат submit

**Signature:**
```typescript
export interface SubmitResult<R> {
  /** Успешно ли выполнен submit */
  success: boolean;
  /** Результат от onSubmit callback */
  data: R | null;
  /** Ошибка, если submit не удался */
  error?: Error;
}
```

_Source: src/core/utils/form-submitter.ts_

### SubmittableForm

**Kind:** `interface`

Интерфейс формы для FormSubmitter
Минимальный контракт для работы с любой формой

**Signature:**
```typescript
export interface SubmittableForm<T extends FormFields> {
  /** Пометить все поля как touched */
  markAsTouched(): void;
  /** Валидировать форму */
  validate(): Promise<boolean>;
  /** Получить значения формы */
  getValue(): T;
}
```

_Source: src/core/utils/form-submitter.ts_

### SubscriptionManager

**Kind:** `class`

Менеджер подписок для FormNode

Централизует управление effect-подписками в узлах формы,
предотвращает утечки памяти и упрощает отладку.

Каждая подписка имеет уникальный ключ, что позволяет:
- Отписываться от конкретной подписки по ключу
- Автоматически заменять существующие подписки
- Отслеживать количество активных подписок (для отладки)

**Signature:**
```typescript
export class SubscriptionManager {
  /**
   * Хранилище подписок
   * Ключ: уникальный идентификатор подписки
   * Значение: функция отписки (dispose)
   */ /* … */ }
```

**Examples:**

```typescript
class FieldNode {
  private subscriptions = new SubscriptionManager();

  watch(callback: Function) {
    const dispose = effect(() => callback(this.value.value));
    return this.subscriptions.add('watch', dispose);
  }

  dispose() {
    this.subscriptions.clear();
  }
}
```

_Source: src/core/utils/subscription-manager.ts_

### syncFields

**Kind:** `function`

Двусторонняя синхронизация двух полей (опционально с трансформом `a → b`).
Сходимость обеспечивается peek-guard'ом; флаг предотвращает лишние круги.

**Signature:**
```typescript
export function syncFields<T>(
  a: Signal<T>,
  b: Signal<T>,
  options?: { transform?: (value: T) => T }
): BehaviorCleanup
```

**Examples:**

```typescript
syncFields(model.$.field1, model.$.field2);
```

_Source: src/core/model/behaviors.ts_

### transformValue

**Kind:** `function`

Трансформация значения поля (идемпотентная): при изменении пишет `transformer(value)` обратно.
Запись отложена (`runOutsideEffect`) во избежание «Cycle detected» (эффект читает и пишет один сигнал).

**Signature:**
```typescript
export function transformValue<T>(
  target: Signal<T>,
  transformer: (value: T) => T
): BehaviorCleanup
```

**Examples:**

```typescript
transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
```

_Source: src/core/model/behaviors.ts_

### uniqueId

**Kind:** `function`

Генерирует уникальный идентификатор с указанным префиксом.

**Signature:**
```typescript
export function uniqueId(prefix: SubscriptionKeyType): string
```

**Parameters:**
- `prefix` — - Префикс для идентификатора (используйте {@link SubscriptionKey}).

**Returns:** Уникальный идентификатор в формате `${prefix}-${counter}`.

**Examples:**

```typescript
import { uniqueId, SubscriptionKey } from '@reformer/core';

uniqueId(SubscriptionKey.WatchField); // → 'watchField-1'
uniqueId(SubscriptionKey.WatchField); // → 'watchField-2'
```

_Source: src/core/utils/unique-id.ts_

### UnknownCallback

**Kind:** `type`

Тип для коллбэков и обработчиков событий
Используется вместо (...args: any[]) => any

**Signature:**
```typescript
export type UnknownCallback = (...args: unknown[]) => unknown;
```

_Source: src/core/types/index.ts_

### UnknownFormValue

**Kind:** `type`

Type-safe alternative to 'any' for unknown form values
Requires explicit type checking before use

**Signature:**
```typescript
export type UnknownFormValue = unknown;
```

_Source: src/core/types/index.ts_

### UnknownRecord

**Kind:** `type`

Тип для Record с unknown значениями
Используется вместо инлайнового `Record<string, unknown>`

**Signature:**
```typescript
export type UnknownRecord = Record<string, any>;
```

_Source: src/core/types/index.ts_

### url

**Kind:** `function`

Фабрика валидатора URL.

Пустые значения пропускаются (используйте `required` для обязательности).

**Signature:**
```typescript
export function url<TForm = unknown, TField extends string | undefined = string>(
  options?: UrlValidatorOptions
): Validator<TForm, TField>
```

**Examples:**

```typescript
validate(path.website, url());
validate(path.website, url({ requireProtocol: true }));
validate(path.website, url({ allowedProtocols: ['https'] }));
```

_Source: src/core/validation/validators/url.ts_

### useArrayLength

**Kind:** `function`

React-хук для подписки только на длину массива.

Оптимизированная версия {@link useFormControl} для ArrayNode, которая
подписывается только на сигнал `length`. Компонент не будет ре-рендериться
при изменении значений вложенных полей.

**Signature:**
```typescript
export function useArrayLength<T extends FormFields>(control: ArrayNode<T>): number
```

**Parameters:**
- `control` — - ArrayNode для подписки

**Returns:** Текущая длина массива

**Examples:**

```tsx
function ArrayRenderer({ arrayNode }) {
  const length = useArrayLength(arrayNode);

  return (
    <div>
      {arrayNode.map((item, index) => (
        <ItemRenderer key={item.id} item={item} />
      ))}
    </div>
  );
}
```

_Source: src/hooks/useArrayLength.ts_

### useFormControl

**Kind:** `function`

React-хук для подписки на состояние формы (FieldNode или ArrayNode).

Обеспечивает реактивную связь между состоянием формы и React-компонентами.
Использует `useSyncExternalStore` для оптимальной интеграции с React 18+
и Concurrent Mode.

#### Основные возможности

- **Автоматическая подписка** на все сигналы контрола
- **Оптимизация ре-рендеров** - компонент обновляется только при реальных изменениях
- **Поддержка SSR** через `useSyncExternalStore`
- **Типобезопасность** - возвращаемый тип зависит от типа контрола

#### Когда использовать

Используйте `useFormControl` когда компоненту нужен доступ к нескольким
свойствам состояния (value, errors, touched и т.д.).

Для подписки только на значение используйте {@link useFormControlValue} -
это предотвратит лишние ре-рендеры при изменении других свойств.

**Signature:**
```typescript
export function useFormControl(
  control: FieldNode<FormValue> | ArrayNode<FormFields> | undefined
): FieldControlState<FormValue> | ArrayControlState<FormFields>
```

**Parameters:**
- `control` — - FieldNode, ArrayNode или undefined

**Returns:** Объект состояния {@link FieldControlState} или {@link ArrayControlState}

**Examples:**

Текстовое поле с валидацией
```tsx
import { useFormControl } from '@reformer/core';
import type { FieldNode } from '@reformer/core';

interface TextFieldProps {
control: FieldNode<string>;
label: string;
}

function TextField({ control, label }: TextFieldProps) {
const {
value,
disabled,
shouldShowError,
errors,
pending
} = useFormControl(control);

return (
<div className="field">
<label>{label}</label>

<div className="input-wrapper">
<input
 type="text"
 value={value}
 disabled={disabled}
 onChange={e => control.setValue(e.target.value)}
 onBlur={() => control.markAsTouched()}
 aria-invalid={shouldShowError}
/>
{pending && <Spinner />}
</div>

{shouldShowError && errors[0] && (
<span className="error" role="alert">
 {errors[0].message}
</span>
)}
</div>
);
}
```

Checkbox с использованием componentProps
```tsx
interface CheckboxProps {
control: FieldNode<boolean>;
}

function Checkbox({ control }: CheckboxProps) {
const { value, disabled, componentProps } = useFormControl(control);

return (
<label className="checkbox">
<input
type="checkbox"
checked={value}
disabled={disabled}
onChange={e => control.setValue(e.target.checked)}
/>
<span>{componentProps.label}</span>
{componentProps.hint && (
<small>{componentProps.hint}</small>
)}
</label>
);
}

// Использование
control.setComponentProps({
label: 'Accept terms and conditions',
hint: 'Required to continue'
});
```

Select с динамическими опциями
```tsx
interface SelectProps {
control: FieldNode<string>;
}

function Select({ control }: SelectProps) {
const { value, disabled, componentProps, shouldShowError, errors } = useFormControl(control);
const options = componentProps.options as Array<{ value: string; label: string }>;

return (
<div>
<select
value={value}
disabled={disabled}
onChange={e => control.setValue(e.target.value)}
onBlur={() => control.markAsTouched()}
>
<option value="">Select...</option>
{options?.map(opt => (
 <option key={opt.value} value={opt.value}>
   {opt.label}
 </option>
))}
</select>
{shouldShowError && <span className="error">{errors[0]?.message}</span>}
</div>
);
}
```

Динамический массив элементов
```tsx
interface Address {
street: string;
city: string;
}

interface AddressListProps {
control: ArrayNode<Address>;
}

function AddressList({ control }: AddressListProps) {
const { length, valid, dirty, errors } = useFormControl(control);

const handleAdd = () => {
control.push({ street: '', city: '' });
};

const handleRemove = (index: number) => {
control.remove(index);
};

return (
<div className="address-list">
<div className="header">
<h3>Addresses ({length})</h3>
{dirty && <span className="badge">Modified</span>}
</div>

{errors.length > 0 && (
<div className="array-errors">
 {errors.map((e, i) => <p key={i}>{e.message}</p>)}
</div>
)}

{control.map((item, index) => (
<AddressItem
 key={item.id}
 control={item}
 onRemove={() => handleRemove(index)}
/>
))}

{length === 0 && (
<p className="empty">No addresses added yet</p>
)}

<button
onClick={handleAdd}
disabled={length >= 5}
>
Add Address
</button>

{!valid && (
<p className="warning">Please fix errors before submitting</p>
)}
</div>
);
}
```

Условный рендеринг с undefined
```tsx
interface FormProps {
optionalField?: ArrayNode<string>;
}

function Form({ optionalField }: FormProps) {
// При undefined возвращается дефолтное состояние
const { length } = useFormControl(optionalField);

if (!optionalField) {
return null;
}

return <div>Items: {length}</div>;
}
```

**See also:**
- {@link useFormControlValue} - для подписки только на значение
- {@link FieldControlState} - тип состояния для FieldNode
- {@link ArrayControlState} - тип состояния для ArrayNode

_Source: src/hooks/useFormControl.ts_

### useFormControlValue

**Kind:** `function`

React-хук для подписки только на значение поля.

Оптимизированная версия {@link useFormControl}, которая подписывается
только на сигнал `value`. Компонент не будет ре-рендериться при изменении
`errors`, `touched`, `valid` и других свойств состояния.

#### Когда использовать

- **Условный рендеринг** на основе значения другого поля
- **Вычисляемые значения** зависящие от значения поля
- **Read-only отображение** значения без интерактивности
- **Оптимизация производительности** когда не нужны другие свойства состояния

#### Когда НЕ использовать

Если компоненту нужны `errors`, `touched`, `disabled` или другие свойства -
используйте {@link useFormControl}. Множественные подписки на один контрол
через разные хуки менее эффективны, чем одна подписка через `useFormControl`.

**Signature:**
```typescript
export function useFormControlValue<T extends FormValue>(control: FieldNode<T>): T
```

**Parameters:**
- `control` — - FieldNode для подписки на значение

**Returns:** Текущее значение поля

**Examples:**

Условный рендеринг секции
```tsx
import { useFormControlValue } from '@reformer/core';

interface FormFields {
hasShipping: FieldNode<boolean>;
shippingAddress: GroupNode<AddressFields>;
}

function ShippingSection({ form }: { form: FormFields }) {
// Подписка только на значение checkbox
const hasShipping = useFormControlValue(form.hasShipping);

if (!hasShipping) {
return null;
}

return (
<div className="shipping-section">
<h3>Shipping Address</h3>
<AddressForm control={form.shippingAddress} />
</div>
);
}
```

Динамические опции на основе другого поля
```tsx
interface FormFields {
country: FieldNode<string>;
city: FieldNode<string>;
}

function CitySelect({ form }: { form: FormFields }) {
const country = useFormControlValue(form.country);
const { value, disabled } = useFormControl(form.city);

// Получаем города для выбранной страны
const cities = useMemo(() => getCitiesForCountry(country), [country]);

// Сбрасываем город при смене страны
useEffect(() => {
form.city.setValue('');
}, [country, form.city]);

return (
<select
value={value}
disabled={disabled || !country}
onChange={e => form.city.setValue(e.target.value)}
>
<option value="">Select city...</option>
{cities.map(city => (
<option key={city.id} value={city.id}>{city.name}</option>
))}
</select>
);
}
```

Отображение суммы в реальном времени
```tsx
interface OrderItem {
quantity: FieldNode<number>;
price: FieldNode<number>;
}

function OrderTotal({ items }: { items: ArrayNode<OrderItem> }) {
// Для каждого элемента получаем только значения
const quantities = items.map(item => useFormControlValue(item.controls.quantity));
const prices = items.map(item => useFormControlValue(item.controls.price));

const total = quantities.reduce((sum, qty, i) => sum + qty * prices[i], 0);

return (
<div className="order-total">
<strong>Total: ${total.toFixed(2)}</strong>
</div>
);
}
```

Preview значения
```tsx
interface MarkdownEditorProps {
control: FieldNode<string>;
}

function MarkdownPreview({ control }: MarkdownEditorProps) {
// Подписка только на значение для preview
const markdown = useFormControlValue(control);

const html = useMemo(() => marked(markdown), [markdown]);

return (
<div
className="markdown-preview"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}

// Основной редактор использует useFormControl для полного состояния
function MarkdownEditor({ control }: MarkdownEditorProps) {
const { value, shouldShowError, errors } = useFormControl(control);

return (
<div className="editor-container">
<textarea
value={value}
onChange={e => control.setValue(e.target.value)}
/>
{shouldShowError && <span className="error">{errors[0]?.message}</span>}

{/* Preview обновляется только при изменении value *}
<MarkdownPreview control={control} />
</div>
);
}
```

Счётчик символов
```tsx
function CharacterCounter({ control, max }: { control: FieldNode<string>; max: number }) {
const value = useFormControlValue(control);
const remaining = max - value.length;

return (
<span className={remaining < 20 ? 'warning' : ''}>
{remaining} characters remaining
</span>
);
}
```

**See also:**
- {@link useFormControl} - для полного состояния поля
- {@link FieldNode} - тип контрола поля

_Source: src/hooks/useFormControlValue.ts_

### ValidateAsyncOptions

**Kind:** `interface`

Опции для функции validateAsync.

**Signature:**
```typescript
export interface ValidateAsyncOptions extends ValidateOptions {
  /** Задержка перед выполнением валидации (в мс) */
  debounce?: number;
}
```

_Source: src/core/types/validation-schema.ts_

### validateFormModel

**Kind:** `function`

In-form валидация: прогоняет {@link validateModel} и роутит ошибки в ноды формы
(через реестр сигнал→нода): `node.setErrors(errors[path] ?? [])` — заодно очищает прошедшие поля
И поля выключенных веток (`{ when, children }` с ложным условием). Дерево обходится ОДИН раз.

**Signature:**
```typescript
export async function validateFormModel<T>(
  model: FormModel<T>,
  schema: unknown
): Promise<ModelValidationResult>
```

_Source: src/core/model/validate-model.ts_

### validateModel

**Kind:** `function`

Headless-валидация данных (sync + async). Работает без UI/нод.

**Signature:**
```typescript
export async function validateModel<T>(
  model: FormModel<T>,
  schema: unknown
): Promise<ModelValidationResult>
```

**Examples:**

```typescript
const res = await validateModel(model, schema);
if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
```

_Source: src/core/model/validate-model.ts_

### validateModelSync

**Kind:** `function`

Синхронная headless-валидация данных. Асинхронные валидаторы пропускаются.

**Signature:**
```typescript
export function validateModelSync<T>(model: FormModel<T>, schema: unknown): ModelValidationResult
```

_Source: src/core/model/validate-model.ts_

### ValidateOptions

**Kind:** `interface`

Опции для функции validate.

**Signature:**
```typescript
export interface ValidateOptions {
  /** Сообщение об ошибке */
  message?: string;
  /** Параметры ошибки */
  params?: FormFields;
}
```

_Source: src/core/types/validation-schema.ts_

### ValidationError

**Kind:** `interface`

Ошибка валидации

**Signature:**
```typescript
export interface ValidationError {
  code: string;
  message: string;
  params?: FormFields;
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
  severity?: 'error' | 'warning';
}
```

_Source: src/core/types/index.ts_

### Validator

**Kind:** `type`

Чистый синхронный валидатор поля.

Принимает значение поля, прокси текущего поля (control) и прокси корня формы (root).
Возвращает ValidationError либо null. Не знает про реестр валидации.

**Signature:**
```typescript
export type Validator<TForm, TField> = (
  value: TField,
  control: FormProxy<TField>,
  root: FormProxy<TForm>
) => ValidationError | null;
```

**Examples:**

```typescript
const isAdult: Validator<MyForm, number> = (value, control, root) => {
  if (value < 18) return { code: 'tooYoung', message: '18+' };
  return null;
};
validate(path.age, isAdult);
```

_Source: src/core/types/validation-schema.ts_

### ValidatorFn

**Kind:** `type`

Синхронная функция валидации

**Signature:**
```typescript
export type ValidatorFn<T = FormValue> = (value: T) => ValidationError | null;
```

_Source: src/core/types/index.ts_

### watchField

**Kind:** `function`

Реакция на изменение поля: вызывает `cb(value)` при каждом изменении (по умолчанию без вызова на
инициализации; `immediate: true` — вызвать сразу).

**Signature:**
```typescript
export function watchField<T>(
  source: ReadonlySignal<T>,
  cb: (value: T) => void,
  options?: { immediate?: boolean }
): BehaviorCleanup
```

**Examples:**

```typescript
watchField(model.$.country, async (country) => {
  model.city = '';
  // ... загрузить города
});
```

_Source: src/core/model/behaviors.ts_

### WithBehaviorSchema

**Kind:** `interface`

Интерфейс для узлов с методом applyBehaviorSchema

**Signature:**
```typescript
export interface WithBehaviorSchema {
  applyBehaviorSchema(schemaFn: unknown): void;
}
```

_Source: src/core/types/index.ts_

### WithValidationSchema

**Kind:** `interface`

Интерфейс для узлов с методом applyValidationSchema

**Signature:**
```typescript
export interface WithValidationSchema {
  applyValidationSchema(schemaFn: unknown): void;
}
```

_Source: src/core/types/index.ts_
