# 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: 6.0.0

## 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
- 18-conditional-fields.md — Условные поля — видимость, доступность и валидация
- 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 — предотвращение «Cycle detected»
- 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)

Архитектура M1: значения живут в **модели** (`createModel`), а форма (`createForm`) строит ноды поверх сигналов модели. Behaviors работают на сигналах (`model.$.field`), а не на строковых путях.

| What                                                                                                   | Where                       |
| ------------------------------------------------------------------------------------------------------ | --------------------------- |
| `createModel`, `createForm`                                                                            | `@reformer/core`            |
| `validateModel`, `validateModelSync`, `validateFormModel`                                              | `@reformer/core`            |
| `useFormControl`, `useFormControlValue`, `useArrayLength`                                              | `@reformer/core`            |
| `FormModel`, `FormProxy`, `FieldNode`, `GroupNode`, `ArrayNode`, `ModelArrayNode`                     | `@reformer/core`            |
| `ModelSignals`, `ModelArray`, `ModelValue`, `ModelObject`, `PathAwareSignal`                          | `@reformer/core`            |
| `ModelValidator`, `ValidationError`, `FieldConfig`, `FormSchema`, `FieldControlState`                 | `@reformer/core`            |
| `computeFrom`, `copyFrom`, `watchField`, `enableWhen`, `disableWhen`                                   | `@reformer/core` (примитивы) |
| `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen`                                          | `@reformer/core` (примитивы) |
| `required`, `min`, `max`, `minLength`, `maxLength`, `email`, `pattern`, `url`, `phone`                | `@reformer/core/validators` |
| `isNumber`, `integer`, `multipleOf`, `nonNegative`, `nonZero`                                          | `@reformer/core/validators` |
| `isDate`, `minDate`, `maxDate`, `pastDate`, `futureDate`, `minAge`, `maxAge`                           | `@reformer/core/validators` |
| `defineFormBehavior`, `compute`, `computeFrom`, `copyFrom`, `onChange`, `enableWhen`, `disableWhen`   | `@reformer/core/behaviors`  |
| `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen`, `apply`, `applyEach`, `aggregateInto`  | `@reformer/core/behaviors`  |
| `exclusiveFlag`, `onDispose`, `getScope`, `effect`, `defer`                                            | `@reformer/core/behaviors`  |

> **Два способа писать behaviors.** Низкоуровневые примитивы (`computeFrom`, `copyFrom`, `watchField`,
> `enableWhen`, …) экспортируются из `@reformer/core`, принимают **сигналы** (`model.$.x`), возвращают
> **cleanup-функцию** и вызываются императивно (например, в `useEffect`). Декларативный DSL
> (`defineFormBehavior` + операторы) экспортируется из `@reformer/core/behaviors`, регистрирует cleanup
> сам и передаётся в `createForm({ behavior })`. См. `20-compute-vs-watch.md`.

> **Валидаторы — чистые фабрики.** `required()`, `min(50000)`, `email()` возвращают функцию
> `(value) => ValidationError | null` и кладутся в поле схемы как `validators: [required(), min(50000)]`.
> Валидация запускается через `validateFormModel(model, schema)` / `validateModel(model, schema)`.

### Type Values

- Опциональные числа: `number | null` (конвенция «пользователь очистил поле»)
- Опциональные строки: `string` (по умолчанию пустая строка) или `string | null`
- Form-shape тип объявляй как `type`-alias — см. `30-type-safety-recipes.md`

### React Hooks Comparison (CRITICALLY IMPORTANT)

| Hook | Return Type | Subscribes To | Use Case |
|------|-------------|---------------|----------|
| `useFormControl(field)` | `{ value, errors, disabled, touched, valid, invalid, pending, shouldShowError, componentProps }` | Все сигналы поля | Полное состояние поля, инпуты |
| `useFormControlValue(field)` | `T` (значение напрямую) | Только сигнал value | Условный рендеринг |
| `useArrayLength(array)` | `number` | Только длина массива | Реактивная длина массива |

**CRITICAL**: Не деструктурируй `useFormControlValue`! Он возвращает `T` напрямую, НЕ `{ 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")`.

Архитектура M1: сначала создаётся **модель данных** (`createModel`), затем **форма**
(`createForm({ model, schema })`), где схема привязывает поля к сигналам модели
(`model.$.field`). Валидаторы — чистые фабрики из `@reformer/core/validators`, лежат
прямо в поле схемы (`validators: [...]`).

```typescript
import { createModel, createForm, validateFormModel, type FormProxy } 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. Model (источник истины значений)
const model = createModel<ContactForm>({ name: '', email: '' });

// 3. Schema: привязка поля к сигналу (model.$.field) + component/componentProps + validators
const schema = {
  name: {
    value: model.$.name,
    component: Input,
    componentProps: { label: 'Name', placeholder: 'Your name' },
    validators: [required({ message: 'Name is required' })],
  },
  email: {
    value: model.$.email,
    component: Input,
    componentProps: { label: 'Email', type: 'email' },
    validators: [required({ message: 'Email is required' }), email({ message: 'Invalid email' })],
  },
};

// 4. Form — ноды поверх сигналов модели
const form = createForm<ContactForm>({ model, schema });

// 5. Use in React component — thin JSX, FormField does ALL heavy lifting
function ContactFormComponent() {
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const result = await validateFormModel(model, schema);
    if (result.valid) {
      console.log('Form submitted:', model.get());
    }
  };

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

// 6. 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" />;
}
```

> **Стабильность инстанса.** В React создавай model/schema/form ОДИН раз через `useMemo(() => { … }, [])`
> — иначе форма пересоздаётся на каждый рендер. См. `28-submit-and-reset.md`, `29-async-preload.md`.

### Arrays of objects — `{ array, item }` schema node

Массивы объектов принадлежат модели (`model.arrayField`). В схеме объявляются узлом
`{ array: model.<path>, item: (itemModel) => itemSchema }`, где `item` строит под-схему
для каждого элемента из его под-модели (`FormModel<Item>`):

```typescript
type PropertyItem = {
  type: 'apartment' | 'house';
  description: string;
  estimatedValue: number;
};

type MyForm = { properties: PropertyItem[] };

const model = createModel<MyForm>({ properties: [] });

// под-схема одного элемента: item.$.field — сигнал под-модели элемента
const propertyItem = (item: FormModel<PropertyItem>) => ({
  type: {
    value: item.$.type,
    component: Select,
    componentProps: { label: 'Тип', options: [/* ... */] },
  },
  description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
  estimatedValue: {
    value: item.$.estimatedValue,
    component: Input,
    componentProps: { label: 'Стоимость', type: 'number' },
  },
});

const schema = {
  properties: { array: model.properties, item: propertyItem },
};

const form = createForm<MyForm>({ model, schema });

// Операции над массивом — на модели:
model.properties.push({ type: 'apartment', description: '', estimatedValue: 0 });
model.properties.removeAt(0);
model.properties.length; // реактивная длина
```

Подробнее в `10-arrays.md`, `21-array-operations.md` и `find_recipe(topic="form-array")`.

### 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

### Model & Form

```typescript
// Модель данных (источник истины значений)
createModel<T extends object>(initial: T): FormModel<T>
// model.get() / model.set(full) / model.patch(partial) / model.isDirty()
// model.reset() / model.captureInitial() / model.signalAt(path)
// model.$.field            → PathAwareSignal<FieldType>  (escape-hatch к сигналу)
// model.arrayField         → ModelArray<Item>            (push/removeAt/insertAt/move/swap/clear/at/map/length)

// Форма (ноды поверх сигналов модели)
createForm<T>({ model, schema, behavior? }): FormProxy<T>
// form.<field>             → FieldNode / GroupNode / FormArrayProxy
// form.<field>.setValue(v) / .value.value / .errors.value / .disabled.value
// form.<field>.enable() / .disable() / .reset() / .markAsTouched() / .setErrors([...])
// form.<field>.updateComponentProps({ ... })

// Валидация данных (headless). schema — то же дерево, что в createForm.
validateFormModel<T>(model, schema): Promise<{ valid: boolean; errors: Record<string, ValidationError[]> }>
validateModel<T>(model, schema): Promise<{ valid; errors }>       // без роутинга ошибок в ноды
validateModelSync<T>(model, schema): { valid; errors }             // async-валидаторы пропускаются
```

### Validators

Валидаторы — **чистые фабрики**: возвращают функцию `(value) => ValidationError | null`.
Кладутся в поле схемы как массив `validators: [required(), min(50000)]`.

```typescript
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
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 validator factories
isDate(options?: { message?: string })
minDate(date: Date | string, options?: { message?: string })
maxDate(date: Date | string, options?: { message?: string })
pastDate(options?: { message?: string })
futureDate(options?: { message?: string })
minAge(years: number, options?: { message?: string })
maxAge(years: number, options?: { message?: string })
```

Использование в схеме:

```typescript
import { createModel, createForm } from '@reformer/core';
import { required, min, max, email } from '@reformer/core/validators';

const model = createModel<{ email: string; age: number; amount: number }>({
  email: '', age: 0, amount: 0,
});

const schema = {
  email:  { value: model.$.email,  component: Input, validators: [required(), email()] },
  age:    { value: model.$.age,    component: Input, validators: [required(), min(18)] },
  amount: { value: model.$.amount, component: Input, validators: [min(0), max(1000)] },
};

const form = createForm({ model, schema });
```

### Custom & cross-field validators

Кастомный валидатор — функция `(value, scope, root) => ValidationError | null | Promise<...>`
(тип `ModelValidator`). `scope` — ближайшая под-модель (элемент массива или корень), `root` —
корневая модель. Cross-field правило читает соседние поля через `root` и вешается на
поле-носитель ошибки:

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

// Value-only
const strongPassword: ModelValidator<string> = (value) =>
  !value || value.length < 8 ? { code: 'too-short', message: 'Минимум 8 символов' } : null;

// Cross-field: сравниваем с другим полем через root
const passwordsMatch: ModelValidator<string, unknown, { password: string }> = (value, _scope, root) =>
  value && root.password && value !== root.password
    ? { code: 'mismatch', message: 'Пароли не совпадают' }
    : null;

// Async (проверка уникальности)
const emailUnique: ModelValidator<string> = async (value) => {
  if (!value) return null;
  const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`);
  const { available } = await res.json();
  return available ? null : { code: 'taken', message: 'Email уже зарегистрирован' };
};

// в схеме
const schema = {
  password: { value: model.$.password, component: Input, validators: [strongPassword] },
  confirm:  { value: model.$.confirm,  component: Input, validators: [passwordsMatch] },
  email:    { value: model.$.email,    component: Input, validators: [emailUnique] },
};
```

### Conditional & array validation (schema tree)

`validateFormModel` обходит дерево схемы. Кроме field-узлов (`{ value, validators }`) движок понимает:

- **условную ветку** `{ when: (scope, root) => boolean, children: [...] }` — поддерево валидируется
  только при истинном `when`; при ложном ошибки полей ветки очищаются;
- **секцию массива** — узел с `componentProps.itemComponent` (`(item) => subSchema`) и `componentProps.control`
  (модель-массив с `at`/`length`) — валидируется per-item со scope = под-модель элемента.

```typescript
// Условная валидация (branch node)
const schema = {
  children: [
    { value: model.$.loanType, validators: [required()] },
    {
      when: (form) => form.loanType === 'mortgage',
      children: [
        { value: model.$.propertyValue, validators: [required(), min(1_000_000)] },
        { value: model.$.initialPayment, validators: [required()] },
      ],
    },
  ],
};
```

> В примерах монорепо эти узлы собираются авторскими хелперами (`field`/`when`/`applyWhen`/`arraySection`)
> — это **локальный typed-сахар в самом примере**, а не экспорт `@reformer/core`. Публичный API — сам
> `validateFormModel(model, schema)` и форма узлов выше.

### Behaviors

Два способа. **Примитивы из `@reformer/core`** (принимают сигналы, возвращают cleanup):

```typescript
computeFrom(sources: ReadonlySignal[], target: Signal, fn: (...vals) => R, options?: { when?: (...vals) => boolean }): () => void
copyFrom(source: ReadonlySignal, target: Signal, options?: { when?: () => boolean; transform?: (v) => v }): () => void
watchField(source: ReadonlySignal, cb: (value) => void, options?: { immediate?: boolean }): () => void
enableWhen(target: ReadonlySignal, condition: () => boolean, options?: { resetOnDisable?: boolean }): () => void
disableWhen(target: ReadonlySignal, condition: () => boolean, options?: { resetOnDisable?: boolean }): () => void
transformValue(target: Signal, transformer: (value) => value): () => void
resetWhen(target: Signal, condition: () => boolean, options?: { resetValue?: T }): () => void
syncFields(a: Signal, b: Signal, options?: { transform?: (v) => v }): () => void
revalidateWhen(deps: ReadonlySignal[], revalidate: () => void): () => void
```

```typescript
import { computeFrom, enableWhen, copyFrom } from '@reformer/core';

const cleanups = [
  computeFrom([model.$.price, model.$.quantity], model.$.total, (p, q) => p * q),
  enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true }),
  copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
];
// при teardown: cleanups.forEach((c) => c());
```

**Декларативный DSL из `@reformer/core/behaviors`** (регистрирует cleanup сам, передаётся в `createForm({ behavior })`):

```typescript
import { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
  compute(model.$.total, () => model.price * model.quantity);
  enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
  onChange(model.$.country, async (country) => {
    form.city.updateComponentProps({ options: await loadCities(country) });
  });
});

const form = createForm({ model, schema, behavior });
```

DSL-операторы: `compute` (auto-tracking, без явного списка источников), `computeFrom`, `copyFrom`,
`onChange` (реакция на изменение; `{ debounce, immediate }`, 2-й аргумент колбэка — `{ signal }` AbortSignal),
`enableWhen`/`disableWhen`, `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen`,
`apply` (под-схема для группы), `applyEach` (per-item для массива), `exclusiveFlag`, `aggregateInto`.
См. `20-compute-vs-watch.md`.

## 4. 3. COMMON PATTERNS

Все паттерны — на архитектуре M1: значения в модели (`model.$.field`), behaviors на сигналах,
валидация через `validateFormModel`.

### Conditional Fields with Auto-Reset

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

// поле включается по условию; при выключении — сброс к initial
enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', {
  resetOnDisable: true,
});
```

Или декларативно внутри `defineFormBehavior`:

```typescript
import { defineFormBehavior, enableWhen } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model }) => {
  enableWhen(
    [model.$.propertyValue, model.$.initialPayment],
    () => model.loanType === 'mortgage',
    { resetOnDisable: true }
  );
});
```

### Computed Field (same or cross level)

`compute`/`computeFrom` пишут в целевой сигнал при изменении источников. Цель не входит
в источники → цикла нет. Кросс-уровневые вычисления работают так же — источники берутся
по сигналам из любого места модели.

```typescript
import { defineFormBehavior, compute, computeFrom } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model }) => {
  // compute: auto-tracking — читаем что нужно прямо из value-модели
  compute(model.$.total, () => (model.price ?? 0) * (model.quantity ?? 0));

  // computeFrom: явный список источников
  computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);

  // кросс-уровневое: fullName из вложенной группы
  compute(model.$.fullName, () =>
    [model.personalData.firstName, model.personalData.lastName].filter(Boolean).join(' ')
  );
});
```

### Async reaction / dynamic options — `onChange`

Для async-реакции на изменение поля (загрузка справочников, зависимые селекты) используй
`onChange` из DSL. Колбэк выполняется вне effect-контекста (можно писать сигналы/ноды), а
2-й аргумент — `{ signal }` (AbortSignal) для отмены устаревших запросов.

```typescript
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
  onChange(
    model.$.region,
    async (region, { signal }) => {
      if (!region) {
        form.city.updateComponentProps({ options: [] });
        return;
      }
      const cities = await fetchCities(region, { signal });
      form.city.updateComponentProps({ options: cities });
    },
    { debounce: 300 }
  );
});
```

> Низкоуровневый аналог — примитив `watchField(model.$.region, cb)` из `@reformer/core` (без debounce/AbortSignal;
> для сети предпочтителен `onChange`). См. `20-compute-vs-watch.md`, `32-async-options-loading.md`.

### Reading values

```typescript
// В React-компоненте — хуки
const { value, errors, disabled } = useFormControl(form.email);
const loanType = useFormControlValue(form.loanType); // значение напрямую

// Вне React — из модели
model.email;             // value-доступ (реактивно внутри effect/computed)
model.$.email.value;     // через сигнал
model.$.email.peek();    // нереактивный снимок
model.get();             // весь объект-снимок
```

### Submit + validation

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

async function handleSubmit(e: React.FormEvent) {
  e.preventDefault();
  const result = await validateFormModel(model, schema); // ошибки роутятся в ноды формы
  if (!result.valid) return;                             // result.errors — { path: ValidationError[] }
  await api.send(model.get());
  model.reset(); // к initial-снимку
}
```

Multi-step: держи отдельные под-схемы на шаг и вызывай `validateFormModel(model, stepSchema)`.
См. `13-multi-step.md`, `28-submit-and-reset.md`.

### Cross-field validation — через `root`

Cross-field правило — `ModelValidator`, читает соседние поля через `root`, вешается на
поле-носитель ошибки:

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

const initialPaymentVsProperty: ModelValidator<number, unknown, MyForm> = (_value, _scope, root) =>
  root.initialPayment && root.propertyValue && root.initialPayment > root.propertyValue
    ? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }
    : null;

const schema = {
  initialPayment: { value: model.$.initialPayment, component: Input, validators: [initialPaymentVsProperty] },
};
```

Чтобы правило перезапускалось при изменении зависимости — добавь `revalidateWhen`:

```typescript
import { revalidateWhen } from '@reformer/core';
revalidateWhen([model.$.propertyValue], () => validateFormModel(model, schema));
```

### Extracting named rules

Когда тело кастомного валидатора или behavior-оператора растёт — выноси в именованную
функцию, типизированную `ModelValidator<TField, TScope, TRoot>`. Схема остаётся плоской и
читается как оглавление:

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

const validateAdultAge: ModelValidator<string> = (value) => {
  if (!value) return null;
  const age = new Date().getFullYear() - new Date(value).getFullYear();
  return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;
};

const schema = {
  birthDate: { value: model.$.birthDate, component: Input, validators: [validateAdultAge] },
};
```

**Naming convention** (camelCase, семантика, не эхо оператора):

- Cross-field правило → инвариант: `initialPaymentVsPropertyValue`, `paymentToIncomeUnderHalf`.
- Field-правило → проверка: `validateAdultAge`, `passwordsMatch`.

## 5. 4. COMMON MISTAKES

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

- Модель/форма/валидация/хуки/типы/**примитивы behaviors** — из `@reformer/core`.
- Чистые фабрики валидаторов — из `@reformer/core/validators`.
- Декларативный DSL (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.

```typescript
// ✅ CORRECT
import {
  createModel,
  createForm,
  validateFormModel,
  useFormControl,
  useFormControlValue,
  type FormProxy,
  type FieldConfig,
  type ModelValidator,
} from '@reformer/core';
import { required, min, max, email } from '@reformer/core/validators';
// либо примитивы behaviors из основного пакета:
import { computeFrom, enableWhen, copyFrom } from '@reformer/core';
// либо декларативный DSL:
import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';
```

> **watchField живёт в `@reformer/core`** (низкоуровневый примитив), НЕ в `@reformer/core/behaviors`.
> В DSL для реакции на изменения используется `onChange`.

### Значения — в модели, не в форме

Под M1 источник истины значений — модель. Форма (ноды) отражает их.

```typescript
// ✅ читаем/пишем значение
model.email = 'a@b.c';       // value-доступ
model.$.email.value;         // сигнал
model.get();                 // весь снимок для submit

// В компоненте — реактивно через хуки
const email = useFormControlValue(form.email);
```

### useFormControlValue (CRITICAL)

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

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

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

### Behaviors принимают СИГНАЛЫ, а не пути/форму

```typescript
// ❌ WRONG - строковые пути и (form) => ... это старый API, удалён
enableWhen(path.city, (form) => Boolean(form.country));
computeFrom(['price', 'quantity'], 'total', (values) => values.price * values.quantity);

// ✅ CORRECT - сигналы модели, условие читает model напрямую
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);
```

### Валидаторы — фабрики в массиве `validators`

```typescript
// ❌ WRONG - нет операторов validate()/applyWhen(); строка вместо options
validate(path.email, required(), 'Email is required');

// ✅ CORRECT - фабрика с options, в поле схемы
const schema = {
  email: {
    value: model.$.email,
    component: Input,
    validators: [required({ message: 'Email is required' }), email()],
  },
};
```

### Cross-field — через `root`, а не `ctx.form`

```typescript
// ❌ WRONG - ctx.form / ctx.setFieldValue / value.value — старый API, не существует
watchField(path.amount, (amount, ctx) => {
  const rate = ctx.form.rate.value.value;
  ctx.setFieldValue('total', amount * rate);
});

// ✅ CORRECT - compute на сигналах; cross-field validator читает root
compute(model.$.total, () => (model.amount ?? 0) * (model.rate ?? 0));

const amountVsMax: ModelValidator<number, unknown, MyForm> = (value, _s, root) =>
  value != null && root.maxAmount != null && value > root.maxAmount
    ? { code: 'tooBig', message: 'Превышает лимит' }
    : null;
```

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

```typescript
// ❌ interface лишён неявной index signature; конструкции ArrayNode<T> его отвергают
export interface PropertyItem {
  type: PropertyType;
  description: string;
}

// ✅ type alias структурно совместим с Record<string, FormValue>
export type PropertyItem = {
  type: PropertyType;
  description: string;
};
```

`number | null` — конвенциональный тип для очищенного поля; встроенные валидаторы
(`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`, `maxAge`) пропускают
пустые значения внутри.

### Proxy не проходит instanceof

```typescript
// ❌ instanceof на Proxy не работает
if (node instanceof FieldNode) { ... }

// ✅ type guards
import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
if (isFieldNode(node)) { ... }
```

## 6. 5. TROUBLESHOOTING

| Error                                                  | Cause                                                    | Solution                                          |
| ------------------------------------------------------ | -------------------------------------------------------- | ------------------------------------------------- |
| `'string' is not assignable to '{ message?: string }'` | Валидатору передали строку вместо options                | Используй `required({ message: 'text' })`         |
| `Module has no exported member`                        | Неверный источник импорта                                | `watchField`/примитивы — из `@reformer/core`; DSL — из `@reformer/core/behaviors`; фабрики — из `@reformer/core/validators` |
| `undefined` из `useFormControlValue`                   | Деструктурировали хук                                    | `const v = useFormControlValue(...)` — без деструктуризации |
| `enableWhen`/`disableWhen` не срабатывает              | Поле не материализовано в форме (элемент массива)        | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |
| `Cycle detected`                                       | Взаимные `compute`/`computeFrom` без стабилизации        | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |
| Значение поля не пишется                               | Пишем в форму вместо модели                              | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` |
| Ошибки не появляются после submit                      | Не вызвали `validateFormModel`                           | `await validateFormModel(model, schema)` — он роутит ошибки в ноды |

## 7. Import Patterns

```typescript
// Модель, форма, валидация, хуки, типы, примитивы behaviors — из @reformer/core
import {
  // фабрики
  createModel,
  createForm,
  // валидация данных (headless)
  validateModel,
  validateModelSync,
  validateFormModel,
  // хуки
  useFormControl,
  useFormControlValue,
  useArrayLength,
  // примитивы behaviors (принимают сигналы, возвращают cleanup)
  computeFrom,
  copyFrom,
  watchField,
  enableWhen,
  disableWhen,
  transformValue,
  resetWhen,
  syncFields,
  revalidateWhen,
} from '@reformer/core';

// Типы
import type {
  FormModel,        // реактивная модель данных
  FormProxy,        // тип формы для props компонентов
  FieldNode,        // узел одного поля
  GroupNode,        // узел группы
  ArrayNode,        // узел массива
  ModelArray,       // реактивный массив модели (push/removeAt/at/map/length)
  ModelSignals,     // дерево сигналов ($)
  PathAwareSignal,  // сигнал, знающий свой путь
  ModelValidator,   // (value, scope, root) => ValidationError | null
  ValidationError,
  FieldConfig,      // { value, component, componentProps?, validators?, ... }
  FormSchema,
  FieldControlState,
} from '@reformer/core';

// Валидаторы — чистые фабрики из /validators
import { required, min, max, email, minLength, pattern } from '@reformer/core/validators';

// Декларативный DSL behaviors — из /behaviors
import {
  defineFormBehavior,
  compute,
  computeFrom,
  copyFrom,
  onChange,
  enableWhen,
  disableWhen,
  transformValue,
  resetWhen,
  syncFields,
  revalidateWhen,
  apply,
  applyEach,
  aggregateInto,
  exclusiveFlag,
} from '@reformer/core/behaviors';
```

### Form-shape тип должен быть `type`, а не `interface`

Прокси `createForm<T>` и типы `ArrayNode<U>` / `GroupNode<U>` требуют, чтобы form-shape
структурно совпадал с `Record<string, FormValue>`. У `interface` нет неявной index signature,
поэтому объявляй form-shape (и типы элементов массива, и вложенные группы) через `type`-alias:

```typescript
export type AddressForm = {
  street: string;
  city: string;
};

export type CoBorrower = {
  fullName: string;
  phone: string;
};
```

См. `30-type-safety-recipes.md`.

## 8. 7. FORM TYPE DEFINITION

Form-shape объявляй через `type`-alias (не `interface` — см. `30-type-safety-recipes.md`).
Тип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без узлов/сигналов.

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

  // Optional fields — конвенция для форм: null («пользователь очистил»)
  phone: string | null;
  age: number | null;

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

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

  // Arrays of objects — model-owned (см. 10-arrays.md)
  items: Array<{
    id: string;
    name: string;
  }>;
};

// Модель создаётся из initial-значений этого типа
const model = createModel<MyForm>({
  name: '',
  email: '',
  phone: null,
  age: null,
  status: 'active',
  address: { street: '', city: '' },
  items: [],
});
```

`number | null` / `string | null` работают со встроенными валидаторами напрямую —
`min`/`max`/`minLength`/`minDate`/`minAge` пропускают пустые значения внутри, guard `if (v != null)`
не нужен.

## 9. 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)

Под M1 схема **привязывает поле к сигналу модели** (`value: model.$.field`) и держит
UI-конфиг (`component`/`componentProps`) + валидаторы. Значения принадлежат модели.

### Field node

```typescript
{
  value: model.$.fieldName,   // сигнал модели (PathAwareSignal) — обязателен
  component: Input,           // React-компонент
  componentProps?: object,    // пропсы (label, placeholder, options, type, ...)
  validators?: [...],         // чистые фабрики / ModelValidator
  asyncValidators?: [...],    // async ModelValidator
  disabled?: boolean,
  updateOn?: 'change' | 'blur' | 'submit',
  debounce?: number,
}
```

> `disabled` — **top-level поле узла** (начальное состояние); дальше управляется методами
> `form.field.disable()`/`enable()` и оператором `enableWhen`. `componentProps.disabled` — это
> UI-проп и на состояние узла **не влияет** (частая причина «computed-поле остаётся редактируемым»).

### Primitive Fields

```typescript
import { createModel, createForm } from '@reformer/core';
import { required } from '@reformer/core/validators';
import { Input, Select, Checkbox } from '@reformer/ui-kit';

const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });

const schema = {
  name: {
    value: model.$.name,
    component: Input,
    componentProps: { label: 'Name', placeholder: 'Enter name' },
    validators: [required()],
  },
  age: {
    value: model.$.age,
    component: Input,
    componentProps: { type: 'number', label: 'Age' },
  },
  agree: {
    value: model.$.agree,
    component: Checkbox,
    componentProps: { label: 'I agree to terms' },
  },
  status: {
    value: model.$.status,
    component: Select,
    componentProps: {
      label: 'Status',
      options: [
        { value: 'active', label: 'Active' },
        { value: 'inactive', label: 'Inactive' },
      ],
    },
  },
};

const form = createForm<MyForm>({ model, schema });
```

### Nested Objects

Вложенная группа модели — это сама по себе под-модель `FormModel<Sub>` (доступна как `model.<group>`,
с `.$`/API). Удобно вынести в builder, принимающий `FormModel<Sub>` — симметрично builder'у элемента
массива. Сигналы под-модели идентичны корневым: `model.address.$.city === model.$.address.city`.

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

const addressNodes = (m: FormModel<Address>) => ({
  street: { value: m.$.street, component: Input, componentProps: { label: 'Street' } },
  city:   { value: m.$.city,   component: Input, componentProps: { label: 'City' } },
  zip:    { value: m.$.zip,    component: Input, componentProps: { label: 'ZIP' } },
});

const schema = {
  address: addressNodes(model.address),
};
```

### Arrays — `{ array, item }` node

Массив объектов объявляется узлом `{ array: model.<path>, item: (itemModel) => subSchema }`.
`item` строит под-схему из под-модели элемента (`FormModel<Item>`):

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

const itemSchema = (item: FormModel<Item>) => ({
  id:   { value: item.$.id,   component: Input, componentProps: { label: 'ID' } },
  name: { value: item.$.name, component: Input, componentProps: { label: 'Name' } },
});

const schema = {
  items: { array: model.items, item: itemSchema },
};
```

### createForm API

```typescript
// M1: данные из модели + схема (+ опциональный декларативный behavior)
const form = createForm<MyForm>({
  model,                 // FormModel<MyForm> — обязателен
  schema,                // дерево узлов, привязанных к сигналам
  behavior: myBehavior,  // опционально: defineFormBehavior(...) из @reformer/core/behaviors
});

// Доступ к нодам через Proxy
form.name.setValue('John');
form.address.city.value.value;  // текущее значение (через сигнал)
model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели
```

> **Тип `schema` в M1 — `unknown`, не `FormSchema<T>`.** Это осознанно: интерпретатор обходит
> произвольную структуру и собирает листья `{ value: signal, component?, ... }`, поэтому обёртки
> (`{ children: [...] }`, wizard/steps) компилируются. Строгий `FormSchema<T>` (keyed-map по полям
> `T`) применяется только на legacy-пути `createForm(schema)` без модели.

### createForm Returns a Proxy

```typescript
const form = createForm<MyForm>({ model, schema });

form.email;          // FieldNode<string> — TypeScript знает тип
form.address.city;   // FieldNode<string> — вложенный доступ
form.items.at(0);    // FormProxy<ItemType> — элемент массива

// IMPORTANT: Proxy не проходит instanceof! Используй type guards:
import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
if (isFieldNode(node)) { /* ... */ }
```

## 10. 9. ARRAY SCHEMA FORMAT

Массивы объектов — **model-owned**: данные принадлежат модели (`model.arrayField` — это
`ModelArray<Item>` с реактивными `push`/`removeAt`/`length`). В схеме массив объявляется узлом
`{ array: model.<path>, item: (itemModel) => subSchema }`, где `item` строит под-схему одного
элемента из его под-модели (`FormModel<Item>`).

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

type Item = { id: string; name: string; price: number };
type MyForm = { items: Item[] };

const model = createModel<MyForm>({ items: [] });

// под-схема одного элемента (item.$.field — сигнал под-модели)
const itemSchema = (item: FormModel<Item>) => ({
  id:    { value: item.$.id,    component: Input },
  name:  { value: item.$.name,  component: Input },
  price: { value: item.$.price, component: Input, componentProps: { type: 'number' } },
});

const schema = {
  items: { array: model.items, item: itemSchema },
};

const form = createForm<MyForm>({ model, schema });
```

> **Type constraint:** тип элемента `Item` объявляй через `type`-alias (не `interface`) — иначе
> он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
> См. `30-type-safety-recipes.md`.

### Один массив — три слоя (три разных движка)

Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
перепутать, но каждая корректна только в своём контексте:

1. **Layout-схема `createForm`** — единственная форма, которую ест `createForm`:
   `{ array: model.<path>, item: (itemModel) => subSchema }`. Массив связывается через
   **value-proxy** `model.properties` (он несёт `__path`), а **не** через сигнальный
   `model.$.properties`.

   ```typescript
   // узел схемы для createForm({ model, schema })
   properties: { array: model.properties, item: propertyItem },
   ```

2. **Validation-схема `validateFormModel`** — секция массива описывается через `componentProps`:
   `{ componentProps: { control: model.<array>, itemComponent: (item) => subSchema } }`. Движок
   узнаёт секцию по `componentProps.itemComponent` + `control` и обходит её per-item.

   ```typescript
   // узел схемы для validateFormModel(model, schema)
   { componentProps: { control: model.properties, itemComponent: propertyRules } }
   ```

3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`ModelArrayNode`),
   а не со схемой: `<FormArray.Root control={form.properties}>` (CDK) или `FormArraySection` из
   `@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).

> **Не путай форму по движку.** `createForm` принимает **только** `{ array, item }`;
> `componentProps.{ control, itemComponent }` — это форма validation-схемы; `FormArray.Root
> control={form.x}` (или `FormArraySection`) — рендер. `control`/`itemComponent` в layout-схеме
> `createForm` не подхватятся, а `{ array, item }` схема валидации не обходит.

### Array operations — на модели

Мутации массива делаются через `ModelArray` (`model.items`), а не через ноду формы:

```typescript
model.items.push({ id: '1', name: '', price: 0 });   // добавить в конец (плоские значения!)
model.items.insertAt(0, { id: '2', name: '', price: 0 });
model.items.removeAt(index);
model.items.move(from, to);
model.items.swap(a, b);
model.items.clear();
model.items.length;                                  // реактивная длина
model.items.at(0);                                   // под-модель элемента (FormModel<Item>)
model.items.map((item, i) => item.name);             // item — FormModel<Item>
```

> **Плоские значения при push.** В `push`/`insertAt` передавай payload из **плоских значений**
> (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
> берутся из `item`-фабрики схемы автоматически.

> **Очистка массива в behavior.** Тот же `model.<array>.clear()` (см. список операций выше) —
> способ очистить коллекцию **вне React**, из behavior: он мутирует модель напрямую, минуя ноды
> формы. Типичный случай — сбросить массив при выключении флага:
>
> ```typescript
> onChange(model.$.hasProperty, (on) => {
>   if (!on) model.properties.clear();
> });
> ```

### Rendering Arrays

Каждый элемент массива — под-форма (`FormProxy<Item>`). Итерируй через `form.items.map`:

```tsx
import { useArrayLength } from '@reformer/core';

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

  return (
    <div>
      {form.items.map((item, index) => (
        <div key={index}>
          <FormField control={item.name} />
          <FormField control={item.price} />
          <button onClick={() => model.items.removeAt(index)}>Remove</button>
        </div>
      ))}

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

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

> В монорепо для массивов используется готовый `FormArraySection` из `@reformer/ui-kit`
> (`control={form.items}`, `itemComponent`, `initialValue`, add/remove/reorder из коробки).
> См. `find_recipe(topic="form-array")`.

### Array Cross-Validation

Cross-field правило по массиву пишется как `ModelValidator`, читает элементы через `root`,
вешается на поле-носитель ошибки (или на первый элемент). Секции массива в схеме валидации
обходятся per-item движком `validateFormModel` (см. `03-api-signatures.md`).

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

// уникальность имён по массиву
const uniqueNames: ModelValidator<unknown, unknown, MyForm> = (_value, _scope, root) => {
  const names = root.items.map((i) => i.name);
  return names.length !== new Set(names).size
    ? { code: 'duplicate', message: 'Item names must be unique' }
    : null;
};
```

## 11. See also

- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item обход `validateFormModel`
- CDK / ui-kit form-array (`FormArray.Root`, `FormArraySection`) — `find_recipe(topic="form-array")`

## 12. 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)

Для async-реакции на изменение поля (загрузка зависимых опций, справочников) используй
`onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно
безопасно писать сигналы/ноды), а 2-й аргумент — `{ signal }` (AbortSignal): при следующей
смене значения предыдущий вызов аннулируется — передавай `signal` в `fetch`.

```typescript
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
  // CORRECT — async onChange со всеми safeguards
  onChange(
    model.$.parentField,
    async (value, { signal }) => {
      if (!value) {
        form.dependentField.updateComponentProps({ options: [] });
        return;
      }
      try {
        const data = await fetchData(value, { signal }); // отмена устаревших запросов
        form.dependentField.updateComponentProps({ options: data });
      } catch (error) {
        if ((error as Error).name === 'AbortError') return;
        form.dependentField.updateComponentProps({ options: [] });
      }
    },
    { debounce: 300 } // не fetch на каждое нажатие
  );
});
```

### Опции onChange

- `debounce: 300` — не дёргать сеть на каждый keystroke (300–500 мс рекомендуется).
- `immediate: true` — вызвать колбэк сразу при регистрации (по умолчанию `false`).
- Guard-clause — пропустить пустое значение.
- try/catch + проверка `AbortError` — обработка отмены и ошибок.

### Низкоуровневый примитив watchField

`watchField` из `@reformer/core` — базовая подписка на изменение сигнала (без debounce и
AbortSignal). `onChange` построен поверх него. Для простых синхронных реакций:

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

// вызывается при каждом изменении (по умолчанию НЕ на инициализации)
const stop = watchField(model.$.country, (country) => {
  model.city = ''; // сброс зависимого поля
});
// stop() — отписаться
```

## 13. 11. ARRAY CLEANUP PATTERN

Очистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на
модели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.

```typescript
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model }) => {
  // при снятии флага — очистить массив
  onChange(model.$.hasItems, (hasItems) => {
    if (!hasItems) model.items.clear();
  });
});
```

Переиспользуемый оператор (как в монорепо):

```typescript
import { onChange } from '@reformer/core/behaviors';
import type { ReadonlySignal } from '@reformer/core/behaviors';

function clearWhenOff(flag: ReadonlySignal<boolean>, array: { clear(): void }): void {
  onChange(flag, (on) => {
    if (!on) array.clear();
  });
}

// в схеме поведения:
clearWhenOff(model.$.hasProperty, model.properties);
```

## 14. 12. MULTI-STEP FORM VALIDATION

Каждый шаг — своя под-схема валидации (дерево узлов `{ children: [...] }`). Переход к
следующему шагу проверяется `validateFormModel(model, stepSchema)`; полный submit — по
общей схеме (объединение шагов). `validateFormModel` роутит ошибки в ноды формы, поэтому
UI подсветит проблемные поля текущего шага автоматически.

```typescript
import { validateFormModel, type FormModel } from '@reformer/core';
import { required, min } from '@reformer/core/validators';

// Под-схема шага: дерево field-узлов { value, validators }
const step1Schema = (m: FormModel<Form>) => ({
  children: [
    { value: m.$.loanType, validators: [required()] },
    { value: m.$.loanAmount, validators: [required(), min(50000)] },
  ],
});

const step2Schema = (m: FormModel<Form>) => ({
  children: [
    { value: m.$.personalData.firstName, validators: [required()] },
    { value: m.$.personalData.lastName, validators: [required()] },
  ],
});

// Карта шагов + полная схема (объединение)
const STEP_SCHEMAS = [step1Schema, step2Schema];
const fullSchema = (m: FormModel<Form>) => ({
  children: STEP_SCHEMAS.map((build) => build(m)),
});
```

```typescript
// Переход к следующему шагу
const goToNextStep = async () => {
  const result = await validateFormModel(model, STEP_SCHEMAS[currentStep - 1](model));
  if (!result.valid) return; // ошибки уже проставлены в ноды текущего шага
  setCurrentStep(currentStep + 1);
};

// Полный submit
const handleSubmit = async () => {
  const result = await validateFormModel(model, fullSchema(model));
  if (result.valid) {
    await onSubmit(model.get());
  }
};
```

### Multi-Step Component Example

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

  const nextStep = async () => {
    const result = await validateFormModel(model, STEP_SCHEMAS[step - 1](model));
    if (result.valid) setStep(step + 1);
  };

  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

### Reuse via apply / applyEach — не дублируй код

```typescript
import { defineFormBehavior, apply, applyEach, compute } from '@reformer/core/behaviors';

// ✅ apply — одна под-схема на несколько групп
const addressBehavior = defineFormBehavior<Address>(({ model }) => {
  compute(model.$.full, () => `${model.city}, ${model.street}`);
});

const behavior = defineFormBehavior<Form>(({ model }) => {
  apply([model.$.homeAddress, model.$.workAddress], addressBehavior);

  // ✅ applyEach — под-схема на КАЖДЫЙ элемент массива (реагирует на add/remove)
  applyEach(
    model.$.items,
    defineFormBehavior<Item>(({ model: row }) => {
      compute(row.$.lineTotal, () => row.qty * row.price);
    })
  );
});
```

### Идемпотентность transformValue

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

// ✅ guard «уже преобразовано»
transformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));
```

### compute для производных полей вместо ручной синхронизации

```typescript
// ❌ ручной onChange + запись — легко зациклить/забыть кейс
onChange(model.$.firstName, () => { model.fullName = `${model.firstName} ${model.lastName}`; });

// ✅ compute: цель не входит в источники → цикла нет, запись идемпотентна (peek-guard)
compute(model.$.fullName, () => [model.firstName, model.lastName].filter(Boolean).join(' '));
```

### Расходящийся цикл compute (Cycle detected)

Взаимные `compute`/`computeFrom` без стабилизации preact обрывает как «Cycle detected».
DSL перехватывает это и бросает понятную ошибку с именем поля. Решение — разорвать цикл
условием `when` или читать одну сторону через `peek()`:

```typescript
// ❌ взаимный пересчёт без стабилизации
compute(model.$.a, () => model.b + 1);
compute(model.$.b, () => model.a + 1); // расходится → Cycle detected

// ✅ добавь стабилизирующее условие или однонаправленную зависимость
compute(model.$.total, () => model.price * model.qty); // одно направление
```

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

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

// Правило вешается на ПОЛЕ-НОСИТЕЛЬ ошибки. Соседние поля читаются через root.
const field1LessThanField2: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
  root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;

const schema = {
  field1: { value: model.$.field1, component: Input, validators: [field1LessThanField2] },
};

// Чтобы правило перезапускалось при изменении field2:
import { revalidateWhen } from '@reformer/core';
revalidateWhen([model.$.field2], () => validateFormModel(model, schema));
```

> **Удалённый API.** Операторы `validate`/`validateAsync`/`applyWhen`/`apply` (валидации),
> `validateGroup`/`validateTree`/`validateForm`, типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn`
> и `ctx.form.*`/`ctx.setFieldValue` — из старой path-based архитектуры и УДАЛЕНЫ. См. `17-nonexistent-api.md`.

## 16. 14. PROJECT STRUCTURE (COLOCATION)

One form is one module folder. **Default layout — flat minimalist:** every concern is a single
file at the module root, and the whole form (entry + all wizard steps inline) lives in one
`index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
carried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a
render-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add
`renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`
nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
and rendered with `FormArraySection` — no per-step component files.

```
src/
├── components/ui/                # App-wide reusable UI (FormField, FormArraySection, ...)
│
├── forms/
│   └── [form-name]/              # Form module — flat, one file per concern
│       ├── index.tsx             # entry + whole form: createModel→createForm→<FormWizard> with all steps inline; arrays via FormArraySection
│       ├── types.ts              # form type + enums + { value, label } option type + constant dictionaries
│       ├── model.ts              # createModel + initial values + empty-array-element factories
│       ├── form.schema.ts        # FormSchema: { value: model.$.x, component, componentProps }
│       ├── form.behavior.ts      # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
│       ├── validation.ts         # ALL validation → { validateStep, validateAll }
│       ├── data-sources.ts       # options + async loaders (dataSources)
│       └── api.ts                # submit + prefill/load
```

Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
`index.tsx`; validation is a single `validation.ts`.** This is the working default for
almost every form — reach for folders only when a file stops fitting on a screen.

### Key Files

```typescript
// forms/credit-application/types.ts
export type CreditApplicationForm = {
  loanType: LoanType;
  loanAmount: number | null;
  // ...
};

// forms/credit-application/model.ts
import { createModel, type FormModel } from '@reformer/core';
export const createCreditApplicationModel = (): FormModel<CreditApplicationForm> =>
  createModel<CreditApplicationForm>(createInitialCreditApplication());

// forms/credit-application/form.schema.ts
import type { FormModel } from '@reformer/core';
export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
  loanType: { value: model.$.loanType, component: Select, componentProps: { /* ... */ } },
  personalData: personalDataNodes(model.$.personalData),
  properties: { array: model.properties, item: propertyItem },
});

// forms/credit-application/form.behavior.ts
import { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';
export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(({ model }) => {
  compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
  enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
});

// forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline
import { createForm } from '@reformer/core';
export const createCreditApplicationForm = () => {
  const model = createCreditApplicationModel();
  return createForm({ model, schema: creditApplicationSchema(model), behavior: creditApplicationBehavior });
};
```

### Scaling up: folders (large forms)

When the flat module gets unwieldy — steps you want in their own files, sub-forms reused across
steps, calc/validators that outgrow one file — promote it to a three-folder module: `lib/`
(domain raw material), `schema/` (the form definition), `components/` (React layout), plus the
entry component and `index.ts`. Keep the module root to just the entry + `index.ts`; everything
else lives in a folder.

```
forms/
└── [form-name]/                  # Form module — folders (scale-up)
    ├── [FormName]Form.tsx        # Entry: builds model+form, renders FormWizard + step components
    ├── index.ts                  # Public re-exports of the module
    │
    ├── lib/                      # Domain helpers (target-agnostic)
    │   ├── types.ts             # Form interface + field enums + { value, label } option type
    │   ├── constants.ts         # Option dictionaries (LOAN_TYPES, GENDERS, ...)
    │   ├── calc.ts              # Pure fns for derived fields (age, monthlyPayment, ...)
    │   ├── custom-validators.ts # Reusable validator factories
    │   └── api.ts               # Data sources + submit
    │
    ├── schema/                   # The form definition
    │   ├── model.ts             # createModel factory + initial values + array-element factories
    │   ├── schema.ts            # createForm schema tree ({ value: model.$.x, component })
    │   ├── validation.ts        # validation schema + validateFormModel config
    │   ├── behavior.ts          # defineFormBehavior(...)
    │   └── create-form.ts       # Assembly: createForm({ model, schema, behavior })
    │
    └── components/
        ├── steps/               # One component per wizard step
        ├── nested-forms/        # Reusable sub-forms (Address, PersonalData, ...)
        └── ui/                  # Form-specific helper blocks (summary, warnings, sections)
```

Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
`schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
may co-locate each step's `schema.ts` / `validation.ts` / `behavior.ts` inside its
`steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
the guide below.

### Scaling

| Complexity | Structure |
| ---------- | --------------------------------------------------------------------------------------- |
| Simple | Single file: `index.tsx` (model + schema + behavior + component) |
| **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |
| Folders (`lib/` + `schema/` + `components/`) | Large forms: split concerns into folders, one component per step, reusable `nested-forms/` |

> The leading layout is configurable: set `REFORMER_FORM_LAYOUT` (`minimalist` | `folders`)
> when registering the MCP server to choose which structure the generators lead with. Default
> is `minimalist`.

> Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
> renderer-json `renderer.schema.json` + `renderer.behavior.ts` + `registry.ts`), the
> centralized-vs-co-located choice, and the full reuse map live in the
> **form-directory-layout** guide (`@reformer/mcp`).

## 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 { createModel, createForm } 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(() => {
    const model = createModel<RegistrationForm>({ email: '', country: 'ru', agree: false });
    const schema = {
      email: {
        value: model.$.email,
        component: Input,
        componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
      },
      country: {
        value: model.$.country,
        component: Select,
        componentProps: {
          label: 'Country',
          options: [
            { value: 'ru', label: 'Россия' },
            { value: 'by', label: 'Беларусь' },
          ],
        },
      },
      agree: {
        value: model.$.agree,
        component: Checkbox,
        componentProps: { label: 'I agree to terms' },
      },
    };
    return createForm<RegistrationForm>({ model, schema });
  }, []);

  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)

**Следующего API НЕТ в @reformer/core** (частично — наследие старой path-based архитектуры,
удалено при переходе на M1):

| Wrong                            | Correct                                          | Notes                                          |
| -------------------------------- | ------------------------------------------------ | ---------------------------------------------- |
| `useForm`                        | `createModel` + `createForm`                     | Хука useForm нет                               |
| `validate`, `validateAsync`      | `validators: [...]` в схеме + `validateFormModel`| Операторов валидации нет; валидаторы — фабрики |
| `applyWhen`, `apply` (валидация) | branch-узел `{ when, children }` в схеме         | Условная валидация — узлом дерева схемы         |
| `validateItems`, `validateGroup`, `validateTree` | секция массива в схеме + `ModelValidator` | Удалены                                  |
| `validateForm`                   | `validateFormModel(model, schema)`               | Legacy-движок удалён                            |
| `ValidationSchemaFn`, `BehaviorSchemaFn` | `defineFormBehavior` / `ModelValidator`  | Типы path-схем удалены                          |
| `FieldPath`, `FieldPathNode`     | `model.$.field` (`PathAwareSignal`)              | Пути заменены сигналами                         |
| `ctx.form.x.value.value`         | `model.x` / `model.$.x.value`                    | В behaviors читаем модель напрямую              |
| `ctx.setFieldValue(name, value)` | `model.x = value` / `compute(...)`               | Не существует                                   |
| `transformers`, `createTransformer` | `transformValue(signal, fn)`                  | Готового набора трансформеров нет               |
| `equalTo`, `custom`, `notEmpty`, `date` (validators) | кастомный `ModelValidator`   | Таких фабрик нет; пиши функцию `(value, scope, root)` |
| `useHiddenCondition`             | `useFormControlValue` + условный рендер в JSX    | Хука нет                                        |
| `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
| `getFieldValue()`                | `model.field` / `useFormControlValue(form.field)`| Не существует                                   |

### Common Import Errors

```typescript
// WRONG - эти символы НЕ существуют
import { useForm, validateForm } from '@reformer/core';           // NO!
import { validate, applyWhen, equalTo } from '@reformer/core/validators'; // NO!
import { transformers } from '@reformer/core/behaviors';          // NO!
import type { FieldPath, ValidationSchemaFn } from '@reformer/core'; // NO!

// CORRECT
import { createModel, createForm, validateFormModel, useFormControl } from '@reformer/core';
import { required, email, min } from '@reformer/core/validators';
import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';
import type { FieldConfig, FieldNode, ModelValidator } from '@reformer/core';
```

### Schema Common Mistakes

```typescript
// WRONG - примитивные значения / value-литерал не работают под M1
const schema = {
  name: '',                       // нет привязки к сигналу
  email: { value: '', ... },      // value должен быть model.$.email, а не литерал
};

// CORRECT - каждое поле привязано к сигналу модели
const schema = {
  name:  { value: model.$.name,  component: Input, componentProps: { label: 'Name' } },
  email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
};
```

### Behaviors Common Mistakes

```typescript
// WRONG - строковые пути и (form) => ... — старый API
enableWhen(path.city, (form) => Boolean(form.country));

// CORRECT - сигналы модели, условие читает model
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
```

## 19. Purpose

«Показать поле X только если Y» под M1 решается тремя независимыми механизмами — выбирай по
тому, что именно должно быть условным:

1. **Видимость / доступность** (поле остаётся в модели, но выключается/включается и не валидируется)
   — behaviors: `enableWhen`, а для условной записи значения — `compute` / `copyFrom` с опцией `{ when }`.
2. **Условная валидация** (правила применяются только в истинной ветке) — нативный branch-узел
   схемы `{ when, children }`, исполняется `validateFormModel`.
3. **Скрытие из разметки** (узел вообще не рендерится) — `useFormControlValue` + условный рендер в JSX.

> Ни `applyWhen`, ни `ValidationSchemaFn` **не экспортируются** из `@reformer/core` — см. раздел
> [Не экспорт (частая ошибка)](#не-экспорт-частая-ошибка) и `17-nonexistent-api.md`.

## 20. Видимость и доступность

`enableWhen(target, () => condition, { resetOnDisable })` из `@reformer/core/behaviors`
включает/выключает поле реактивно. При `disabled` поле не участвует в валидации; с
`resetOnDisable: true` его значение сбрасывается при выключении (иначе — сохраняется).

```typescript
import { defineFormBehavior, enableWhen, compute, copyFrom } from '@reformer/core/behaviors';

type CreditForm = {
  loanType: 'mortgage' | 'car' | 'consumer';
  propertyValue: number | null;
  initialPayment: number | null;
  sameEmail: boolean;
  email: string;
  emailAdditional: string;
};

export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
  // один или несколько таргетов; условие читает model.* реактивно
  enableWhen(
    [model.$.propertyValue, model.$.initialPayment],
    () => model.loanType === 'mortgage',
    { resetOnDisable: true },
  );

  // условная ЗАПИСЬ производного значения — compute с { when }
  compute(model.$.initialPayment, () => Math.round((model.propertyValue ?? 0) * 0.2), {
    when: () => model.loanType === 'mortgage',
  });

  // условное КОПИРОВАНИЕ из другого поля — copyFrom с { when }
  copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true });
});
```

Требования:

- **Все поля должны быть материализованы в модели** (`createModel`), даже те, что показываются
  только под условием. `enableWhen`/`compute`/`copyFrom` принимают сигналы `model.$.field`, а не
  строковые пути; сигнала для несуществующего поля нет.
- Поведение подключается к форме через `createForm({ model, schema, behavior })` — форма владеет
  жизненным циклом, DSL-операторы регистрируют свой cleanup сами (ручной массив cleanup'ов не нужен).
- Таргетом может быть и **группа**: `enableWhen(model.$.residenceAddress, () => model.sameAsRegistration === false)`
  проходит по поддереву; без `resetOnDisable` значение группы сохраняется (удобно, когда оно копируется
  из другого источника).

> **Групповой таргет работает только у DSL-`enableWhen` из `@reformer/core/behaviors`**, не у
> одноимённого корневого `enableWhen` из `@reformer/core` (тот резолвит только leaf-сигналы через
> реестр → на group-сигнале это тихий no-op). `get_symbol_docs("enableWhen")` возвращает именно
> корневой, не group-capable вариант.

## 21. Условная валидация

Правила, действующие только при выполнении условия, задаются **нативным branch-узлом**
`{ when: (scope, root) => boolean, children: [...] }` прямо в дереве схемы. `validateFormModel`
обходит дерево: при истинном `when` валидирует `children`, при ложном — **поддерево пропускается,
а ошибки его полей очищаются** (движок вызывает `setErrors([])`). `scope` — ближайшая под-модель
(элемент массива или корень), `root` — корневая модель формы.

```typescript
import { validateFormModel } from '@reformer/core';
import { required, min } from '@reformer/core/validators';

const schema = {
  children: [
    { value: model.$.loanType, validators: [required()] },
    {
      // ветка применяется только для ипотеки
      when: (_scope, root) => root.loanType === 'mortgage',
      children: [
        { value: model.$.propertyValue, validators: [required(), min(1_000_000)] },
        { value: model.$.initialPayment, validators: [required()] },
      ],
    },
  ],
};

const { valid, errors } = await validateFormModel(model, schema);
```

Переключение `loanType` с `mortgage` на другой тип автоматически снимает ошибки `required` с
`propertyValue`/`initialPayment` — их держать enabled и не валидировать помогает `enableWhen` выше.

## 22. Скрытие в JSX

Чтобы условный блок вообще не рендерился, читай значение-триггер хуком `useFormControlValue(form.field)`
и делай ранний `return null`. Хук возвращает **только значение** (без деструктуризации `{ value }`).

```tsx
import { useFormControlValue } from '@reformer/core';

function MortgageFields({ form }: { form: CreditForm }) {
  const loanType = useFormControlValue(form.loanType);
  if (loanType !== 'mortgage') return null;

  return (
    <>
      <PropertyValueField form={form} />
      <InitialPaymentField form={form} />
    </>
  );
}
```

Скрытие в JSX — чисто визуальное: если поле должно ещё и **выпадать из валидации/состояния**,
сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или branch-узлом (валидация).

## 23. Не экспорт (частая ошибка)

- **`applyWhen` — это ЛОКАЛЬНЫЙ typed-хелпер примера, а не экспорт `@reformer/core`.** Он лишь
  эмитит нативный branch-узел `{ when, children }`. Каноничное определение (см.
  `complex-multy-step-form/schemas/validation.ts`):

  ```typescript
  // локальный сахар в самом примере — НЕ импорт из библиотеки
  const applyWhen = (cond: (form: Root) => boolean, children: SchemaNode[]): SchemaNode => ({
    when: (_scope: unknown, root: unknown) => cond(root as Root),
    children,
  });
  ```

  Публичный API — сам `validateFormModel(model, schema)` и форма узла `{ when, children }`.
  Не пиши `import { applyWhen } from '@reformer/core'` — такого символа нет.

- **`ValidationSchemaFn` не существует** (тип старой path-схемы удалён при переходе на M1). Тип
  кастомного/cross-field валидатора — **`ModelValidator<TValue, TModel, TRoot>`**:

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

  // (value, scope, root) — cross-field читает соседей через root
  const passwordsMatch: ModelValidator<string, unknown, { password: string }> = (value, _scope, root) =>
    value && root.password && value !== root.password
      ? { code: 'mismatch', message: 'Пароли не совпадают' }
      : null;
  ```

## 24. See also

- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom`, branch-узел, `ModelValidator`
- [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, …)
- [25-reset-when.md](./25-reset-when.md) — `resetWhen` как альтернатива, когда поле остаётся enabled
- [19-reading-values.md](./19-reading-values.md) — `useFormControlValue` и чтение значений в React

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

Под M1 значения живут в модели. Есть три контекста чтения: value-доступ модели, сигналы, и React-хуки.

### В React-компоненте — хуки

```typescript
// Полное состояние поля (объект)
const { value, errors, disabled, touched, shouldShowError } = useFormControl(control.email);

// Только значение (напрямую, БЕЗ деструктуризации!)
const email = useFormControlValue(control.email);

// Реактивная длина массива
const count = useArrayLength(control.items);
```

### Вне React — модель

```typescript
// value-доступ (реактивно внутри effect/computed, запись присваиванием)
model.email;                 // читать
model.email = 'a@b.c';       // писать
model.address.city;          // вложенное поле (model.address — под-модель FormModel<Address>)

// через сигнал (escape-hatch)
model.$.email.value;         // реактивное чтение/запись
model.$.email.peek();        // нереактивный снимок
model.$.address.city.value;  // сигнал вложенного поля (≡ model.address.$.city у под-модели)

// весь объект
model.get();                 // снимок { email, address: { city }, ... } — для submit
```

### В behaviors — читаем model напрямую

`compute`/`onChange`/условия `when` читают значения из value-модели (`model.field`) —
подписка на сигналы происходит автоматически внутри реактивного эффекта.

```typescript
import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
  // читаем несколько полей — compute сам подпишется на прочитанные сигналы
  compute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);

  // onChange: 1-й аргумент — новое значение; остальные поля берём из model
  onChange(model.$.loanAmount, (amount) => {
    const term = model.loanTerm;
    if (amount && term) {
      form.monthlyPayment.updateComponentProps({ hint: `≈ ${amount / term}` });
    }
  });
});
```

> Cross-field ЗАПИСЬ производного значения делай через `compute` (цель не входит в источники →
> цикла нет). Для side-эффектов (загрузка опций, обновление componentProps) — `onChange`.
> `computeFrom`/`copyFrom`/`enableWhen` принимают сигналы (`model.$.x`), НЕ строковые пути.

## 26. 17. COMPUTE vs ONCHANGE

Под M1 behaviors работают на сигналах модели. Есть два способа их писать:

- **примитивы из `@reformer/core`** — принимают сигналы, возвращают cleanup, вызываются
  императивно (например, в `useEffect`), cleanup складывается в массив;
- **декларативный DSL из `@reformer/core/behaviors`** — `defineFormBehavior(...)` + операторы,
  cleanup управляется формой, передаётся в `createForm({ behavior })`.

Для производных значений — `compute`/`computeFrom`. Для side-эффектов на изменение (async,
обновление componentProps) — `onChange` (DSL) или примитив `watchField`.

### compute — auto-tracking (DSL)

`compute(target, read)` подписывается на сигналы, прочитанные внутри `read()`, и пишет
результат в `target`. Цель не входит в источники → цикла нет; запись идемпотентна (peek-guard).
Кросс-уровневые вычисления работают так же — читай любые поля модели.

```typescript
import { defineFormBehavior, compute } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model }) => {
  // same-level
  compute(model.$.total, () => (model.price ?? 0) * (model.quantity ?? 0));

  // nested-to-nested / cross-level — просто читаем нужные поля
  compute(model.$.fullName, () =>
    [model.personalData.firstName, model.personalData.lastName].filter(Boolean).join(' ')
  );

  // условный пересчёт
  compute(model.$.initialPayment, () => model.propertyValue * 0.2, {
    when: () => model.loanType === 'mortgage',
  });
});
```

> **Не читай `model.get()` внутри `compute`/`when`** — это нереактивный снимок, зависимость не
> отследится и пересчёта не будет. Читай поля по отдельности (`model.field` или `model.$.field.value`).
> `model.get()` — только вне реактивного контекста (в `onChange`, обработчиках событий).

### Computed sum over a FormArray

Агрегат по массиву (сумма доходов созаёмщиков и т.п.) считается **реактивно** через value-proxy
массива — `model.<array>.map(...)`:

```typescript
compute(model.$.coBorrowersIncome, () =>
  model.coBorrowers.map((cb) => cb.monthlyIncome ?? 0).reduce((s, v) => s + v, 0)
);
```

Почему реактивно: `.map` читает сигнал самого массива (трекает `push`/`removeAt`/reorder) **и**
внутри колбэка читает каждый `cb.<field>` (трекает правки элементов) → пересчёт срабатывает на оба
вида изменений.

Ключевой нюанс — **на каком proxy** живёт `.map`/`.reduce`(-via-`.map`):

- **value-proxy `model.coBorrowers`** — есть `.map`/`.forEach`/`.at`/индексы (обходит элементы как
  значения). Именно его читай в `compute`.
- **signals-proxy `model.$.coBorrowers`** — только индексный доступ и `.length`, без `.map`. Для
  агрегата не подходит.

Если нужен только **счётчик** (пересчёт лишь на изменение длины, без чтения полей элементов) —
`model.<array>.map(() => null)` (длина трекается, значения — нет):

```typescript
// зависит только от КОЛИЧЕСТВА элементов, не от их полей
compute(model.$.interestRate, () => computeRate(model.properties.map(() => null).length));
```

> **Не агрегируй через `model.get()` / `model.<array>.peek()`** в `compute` — это нереактивный
> снапшот массива, зависимость не отследится и сумма не пересчитается. Читай массив только через
> value-proxy `model.<array>.map(...)`.

### computeFrom — явный список источников

Когда нужен явный контроль зависимостей — `computeFrom(sources, target, fn)`. Значения
источников приходят в `fn` позиционно.

```typescript
import { computeFrom } from '@reformer/core/behaviors'; // или из '@reformer/core' как примитив

computeFrom(
  [model.$.loanAmount, model.$.loanTerm, model.$.interestRate],
  model.$.monthlyPayment,
  (amount, term, rate) => annuityMonthly(amount ?? 0, term ?? 0, rate ?? 0)
);
```

> Примитив `computeFrom` из `@reformer/core` имеет ту же сигнатуру и возвращает cleanup-функцию.

### onChange — реакция на изменение (async, side-effects)

`onChange(source, cb, { debounce, immediate })` вызывает `cb(value, { signal })` при изменении.
Колбэк выполняется ВНЕ effect-контекста — можно писать сигналы/ноды. `signal` (AbortSignal)
аннулируется при следующей смене значения.

```typescript
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
  onChange(
    model.$.country,
    async (country, { signal }) => {
      const cities = await fetchCities(country, { signal });
      form.city.updateComponentProps({ options: cities });
    },
    { debounce: 300 }
  );
});
```

### Примитив watchField

Низкоуровневая подписка из `@reformer/core` (без debounce/AbortSignal). `onChange` построен
поверх неё. Для простых синхронных реакций:

```typescript
import { watchField } from '@reformer/core';
const stop = watchField(model.$.country, () => { model.city = ''; });
```

### Rule of Thumb

| Scenario | Use |
|----------|-----|
| Производное значение (любой уровень) | `compute` (auto-tracking) |
| Производное с явными зависимостями | `computeFrom` |
| Async-реакция, обновление componentProps | `onChange` (debounce + AbortSignal) |
| Простая синхронная реакция (примитив) | `watchField` |

### Chained computeds

Если несколько вычислений зависят друг от друга (`interestRate` → `monthlyPayment` →
`paymentToIncomeRatio`), объяви каждое отдельным `compute` — они выстроятся в правильном
порядке через реактивный граф (цель одного = источник другого). Расходящиеся взаимные
`compute` без стабилизации бросят понятную ошибку (см. `22-cycle-detection.md`).

## 27. 18. ARRAY OPERATIONS

Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);
рендер — через ноду формы (`form.items.map` / `.at`).

### Array Access

```typescript
// Через ModelArray (данные)
model.items.at(0);              // FormModel<Item> | undefined (под-модель элемента)
model.items.map((item, i) => item.name);  // item — FormModel<Item>
model.items.length;             // реактивная длина
model.items.toArray();          // снимок значений

// Через ноду формы (рендер / доступ к нодам полей)
form.items.at(0);               // FormProxy<Item> | undefined
form.items.map((item, i) => …); // item — FormProxy<Item>
```

### Array Methods (на модели)

```typescript
model.items.push({ name: '', price: 0 });        // добавить в конец (ПЛОСКИЕ значения!)
model.items.insertAt(0, { name: '', price: 0 }); // вставить по индексу
model.items.removeAt(index);                     // удалить по индексу
model.items.move(fromIndex, toIndex);            // переместить
model.items.swap(a, b);                          // поменять местами
model.items.clear();                             // очистить
```

> **push принимает ПЛОСКИЕ значения** (`{ name, price }`), а НЕ FieldConfig-шаблон
> (`{ value, component }`). Component/componentProps берутся из `item`-фабрики схемы.
> Передача FieldConfig-объектов сломает рендер (`[object Object]` в инпутах).

### Rendering Arrays

```tsx
import { useArrayLength } from '@reformer/core';

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

  return (
    <div>
      {form.items.map((item, index) => (
        <div key={index}>
          <FormField control={item.name} />
          <FormField control={item.price} />
          <button onClick={() => model.items.removeAt(index)}>Remove</button>
        </div>
      ))}
      {length === 0 && <p>No items yet</p>}
      <button onClick={() => model.items.push({ name: '', price: 0 })}>Add Item</button>
    </div>
  );
}
```

> В монорепо используется `FormArraySection` из `@reformer/ui-kit`: `control={form.items}`,
> `itemComponent`, `initialValue`, add/remove/reorder из коробки. См. `find_recipe(topic="form-array")`.

### Per-item behavior — applyEach

Чтобы применить поведение к КАЖДОМУ элементу (реагируя на add/remove), используй `applyEach`:

```typescript
import { defineFormBehavior, applyEach, compute } from '@reformer/core/behaviors';

const behavior = defineFormBehavior<MyForm>(({ model }) => {
  applyEach(
    model.$.items,
    defineFormBehavior<Item>(({ model: row }) => {
      compute(row.$.lineTotal, () => row.qty * row.price); // per-row value-op
    })
  );
});
```

### Aggregate write — aggregateInto

Агрегатная запись в строки (например, «последняя строка = 100 − Σ остальных»):

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

aggregateInto(model.$.rows, (rows) => {
  const n = rows.length;
  if (n === 0) return [];
  const others = rows.slice(0, n - 1).reduce((s, r) => s + r.percent, 0);
  return [{ index: n - 1, patch: { percent: 100 - others } }]; // derive должна сходиться
});
```

### Array Cross-Validation

Cross-field правило по массиву — `ModelValidator`, читает элементы через `root`:

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

const percentagesSumTo100: ModelValidator<unknown, unknown, MyForm> = (_v, _s, root) => {
  const total = root.items.reduce((sum, i) => sum + (i.percentage || 0), 0);
  return Math.abs(total - 100) > 0.01
    ? { code: 'invalid_total', message: 'Percentages must sum to 100%' }
    : null;
};
```

## 28. Как устроено под M1

Behaviors на сигналах уже защищены от типичных циклов:

- `compute`/`computeFrom` пишут в цель только если значение изменилось (**peek-guard**), а цель
  не входит в источники → сходящийся пересчёт не зацикливается;
- `transformValue`/`resetWhen`/`syncFields`/`enableWhen` откладывают запись состояния вне
  effect-контекста (`runOutsideEffect` / микротаск) — эффект «читает и пишет один сигнал» не падает;
- `onChange`-колбэк выполняется вне effect-контекста, поэтому в нём можно свободно писать сигналы/ноды.

Тебе НЕ нужно вручную ставить `{ immediate: false }`, guard'ить `disabled.value` перед
`disable()` или сравнивать значения перед записью — это делается внутри операторов.

## 29. Когда всё-таки возникает «Cycle detected»

### 1. Расходящийся взаимный compute

Два вычисления, которые бесконечно гоняют значение друг у друга без стабилизации:

```typescript
// ❌ расходится → preact бросает «Cycle detected»
compute(model.$.a, () => model.b + 1);
compute(model.$.b, () => model.a + 1);
```

DSL перехватывает это и заменяет понятной ошибкой с именем поля и подсказкой. Решение —
однонаправленная зависимость или стабилизирующее условие `when`:

```typescript
// ✅ одно направление
compute(model.$.total, () => model.price * model.qty);

// ✅ стабилизация условием
compute(model.$.a, () => model.b + 1, { when: () => model.a !== model.b + 1 });
```

### 2. Неидемпотентный transformValue

`transformValue` пишет обратно в то же поле. Если `f(f(x)) !== f(x)` — цикл:

```typescript
// ❌ f(f(x)) = "prefix-prefix-x" ≠ f(x)
transformValue(model.$.field, (v) => `prefix-${v}`);

// ✅ guard «уже преобразовано»
transformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));
```

### 3. Условие `resetWhen`/`copyFrom`, читающее собственную цель

Если условие зависит от значения целевого поля — сброс/копия триггерит своё же условие:

```typescript
// ❌ самотриггер — условие читает cardNumber (цель)
resetWhen(model.$.cardNumber, () => model.cardNumber !== '');

// ✅ условие зависит только от независимого поля
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
```

## 30. Prefer built-in operators over manual logic

Вместо ручной сборки reset-on-disable через `onChange` — используй `enableWhen({ resetOnDisable })`:

```typescript
// ✅ SIMPLE — enableWhen с resetOnDisable (рекомендуется)
enableWhen(model.$.vehicleVin, () => model.insuranceType === 'casco', { resetOnDisable: true });
enableWhen(model.$.vehicleBrand, () => model.insuranceType === 'casco', { resetOnDisable: true });
```

`enableWhen` принимает и **массив** целей, и группу — можно включать несколько полей одним условием:

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

## 31. Key Rules

1. Производные значения — через `compute`/`computeFrom` (peek-guard встроен), не ручным `onChange` + запись.
2. `transformValue`-трансформер должен быть идемпотентным (`f(f(x)) === f(x)`).
3. Условие `resetWhen`/`copyFrom`/`enableWhen` НЕ должно читать собственную цель.
4. Расходящийся взаимный `compute` — разорви однонаправленной зависимостью или `when`.

## 32. Purpose

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

## 33. API

Есть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):

```typescript
function copyFrom<T>(
  source: ReadonlySignal<T>,
  target: Signal<T>,
  options?: { when?: () => boolean; transform?: (value: T) => T }
): () => void; // возвращает cleanup
```

**DSL-оператор из `@reformer/core/behaviors`** (сигнал ИЛИ группа):

```typescript
function copyFrom<T>(
  source: ReadonlySignal<T> | object,   // model.$.field или model.$.group
  target: Signal<T> | object,
  options?: { when?: () => boolean; transform?: (value: T) => T }
): void; // cleanup управляется формой
```

`when` — реактивное условие (читает сигналы модели через `model.*`). `transform` применяется
к значению перед записью в target. Отдельной опции `fields`/`debounce` нет.

## 34. Examples

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

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

type OrderForm = {
  useShippingAsBilling: boolean;
  shippingAddress: string;
  billingAddress: string;
};

export const orderBehavior = defineFormBehavior<OrderForm>(({ model }) => {
  copyFrom(model.$.shippingAddress, model.$.billingAddress, {
    when: () => model.useShippingAsBilling === true,
  });
});
```

Пример: `copyFrom(model.$.shippingAddress, model.$.billingAddress, { when: () => model.useShippingAsBilling })`.

### Копирование группы целиком

Когда source/target — группы (`model.$.registrationAddress`), копируется всё значение группы:

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

type ProfileForm = {
  sameAsRegistration: boolean;
  registrationAddress: Address;
  residenceAddress: Address;
};

export const profileBehavior = defineFormBehavior<ProfileForm>(({ model }) => {
  copyFrom(model.$.registrationAddress, model.$.residenceAddress, {
    when: () => model.sameAsRegistration === true,
  });
});
```

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

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

type ContactForm = { sameEmail: boolean; email: string; emailAdditional: string };

export const contactBehavior = defineFormBehavior<ContactForm>(({ model }) => {
  copyFrom(model.$.email, model.$.emailAdditional, {
    when: () => model.sameEmail === true,
    transform: (value) => (typeof value === 'string' ? value.trim().toLowerCase() : value),
  });
});
```

### Как примитив (вне defineFormBehavior)

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

const cleanups = [
  copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
];
// teardown: cleanups.forEach((c) => c());
```

## 35. Anti-patterns

```typescript
// ❌ Двусторонняя связь через два copyFrom — конфликт направлений
copyFrom(model.$.a, model.$.b);
copyFrom(model.$.b, model.$.a);

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

```typescript
// ❌ when, читающий саму target — лишний триггер при перезаписи target
copyFrom(model.$.source, model.$.target, { when: () => model.target === '' });

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

## 36. Troubleshooting

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

**Q: «Cycle detected» при копировании.**
A: Обычно `when` читает значение target (см. anti-pattern). Условие должно зависеть только от
source и независимых флагов.

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

## 37. See also

- [24-sync-fields.md](./24-sync-fields.md) — двусторонняя синхронизация
- [25-reset-when.md](./25-reset-when.md) — сброс target при выключенном условии
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
- [22-cycle-detection.md](./22-cycle-detection.md) — почему `runOutsideEffect` защищает от циклов

## 38. Purpose

`syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них
переписывает второе. Применяется для дублей одного значения в разных частях формы
(тех. поле + видимое представление, mirror-поля). Внутренний флаг + `runOutsideEffect`
исключают петли. Для одностороннего копирования — [`copyFrom`](./23-copy-from.md); для
расчётов — [`compute`](./20-compute-vs-watch.md).

## 39. API

Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):

```typescript
// примитив: возвращает cleanup
function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): () => void;

// DSL: cleanup управляется формой
function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (value: T) => T }): void;
```

`a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:
применяется только при движении значения `a → b`. Опции `debounce`/`when` нет.

## 40. Examples

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

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

type MirrorForm = { syncField1: string; syncField2: string };

export const mirrorBehavior = defineFormBehavior<MirrorForm>(({ model }) => {
  syncFields(model.$.syncField1, model.$.syncField2);
});
```

Пример: `syncFields(model.$.syncField1, model.$.syncField2)`.

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

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

type DisplayForm = { internalCode: string; displayCode: string };

export const codeBehavior = defineFormBehavior<DisplayForm>(({ model }) => {
  // internalCode → displayCode: uppercase; обратно значение пишется как есть
  syncFields(model.$.internalCode, model.$.displayCode, {
    transform: (value) => (typeof value === 'string' ? value.toUpperCase() : value),
  });
});
```

### Как примитив (вне defineFormBehavior)

```typescript
import { syncFields } from '@reformer/core';
const stop = syncFields(model.$.syncField1, model.$.syncField2);
// stop() — отписаться
```

## 41. Anti-patterns

```typescript
// ❌ Симметрично через два copyFrom — конфликт направлений
copyFrom(model.$.a, model.$.b);
copyFrom(model.$.b, model.$.a);

// ✅ syncFields умеет двустороннюю связь без петель
syncFields(model.$.a, model.$.b);
```

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

// ✅ Симметричные трансформы — syncFields + transformValue на обоих полях
syncFields(model.$.a, model.$.b);
transformValue(model.$.a, (v) => (typeof v === 'string' ? v.trim() : v));
transformValue(model.$.b, (v) => (typeof v === 'string' ? v.trim() : v));
```

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

// ✅ Для конвертации — compute в обе стороны или один канонический формат + computed отображение
compute(model.$.amountNumber, () => Number(model.amountString));
```

## 42. Troubleshooting

**Q: Поля «дёргаются», несколько перезаписей.**
A: Чаще всего на одном из полей висит `transformValue`/`compute`. Убедитесь, что transform
идемпотентен (`f(f(x)) === f(x)`).

**Q: «Cycle detected» при `syncFields`.**
A: Не вешайте дополнительно `onChange`/`watchField`, которые сами пишут в эти же поля.
`syncFields` уже занимает оба направления.

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

## 43. 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 ломается
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений

## 44. Purpose

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

## 45. API

Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):

```typescript
// примитив: возвращает cleanup
function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): () => void;

// DSL: cleanup управляется формой
function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { resetValue?: T }): void;
```

`target` — сигнал (`model.$.field`). `condition` — реактивное условие (читает `model.*`).
`resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
поля не трогаются оператором (значения принадлежат модели).

## 46. Examples

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

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

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

export const checkoutBehavior = defineFormBehavior<CheckoutForm>(({ model }) => {
  resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
});
```

Пример: `resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' })`.

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

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

type MortgageForm = { propertyValue: number | null; initialPayment: number };

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

### Как примитив (вне defineFormBehavior)

```typescript
import { resetWhen } from '@reformer/core';
const stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
```

## 47. Anti-patterns

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

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

```typescript
// ❌ resetValue с типом, отличным от поля
resetWhen(model.$.amount, () => model.skipPayment, { resetValue: 'none' }); // amount: number ← string

// ✅ resetValue совместим с типом поля
resetWhen(model.$.amount, () => model.skipPayment, { resetValue: 0 });
```

```typescript
// ❌ Для строкового поля без resetValue прилетит null, а Input ждёт string
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card');

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

```typescript
// ❌ condition читает саму цель — самотриггер (см. 22-cycle-detection.md)
resetWhen(model.$.cardNumber, () => model.cardNumber !== '');

// ✅ condition зависит только от независимого поля
resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
```

## 48. Troubleshooting

**Q: Сброс не срабатывает, хотя `condition` возвращает true.**
A: Проверьте, что behavior зарегистрирован (`createForm({ behavior })`) или примитив не отписан,
и что `condition` читает реактивные поля (`model.*`), а не снимок `model.get()`.

**Q: Реактивная цепочка resetWhen → compute → resetWhen ломается.**
A: Убедитесь, что `condition` не зависит от значения самого поля, иначе после сброса попадёте
в новый триггер.

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

## 49. 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` не должен читать целевое поле

## 50. Purpose

`transformValue` подписывается на изменение поля и переписывает его трансформированной
версией: uppercase для кодов, trim+toLowerCase для email, округление для чисел. Применяется
единообразно ко всем источникам изменения (пользователь, `model.field = …`, `model.set/patch`,
`copyFrom`). **Идемпотентность (`f(f(x)) === f(x)`) обязательна** — иначе бесконечный цикл.
Оператор откладывает запись вне effect-контекста (`runOutsideEffect`) и не пишет, если
`transformer(value) === value` — базовый guard от циклов.

## 51. API

Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):

```typescript
// примитив: возвращает cleanup
function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): () => void;

// DSL: cleanup управляется формой
function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): void;
```

`target` — сигнал (`model.$.field`). `transformer` — чистая идемпотентная функция значения.
Дополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора
`transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.

## 52. Examples

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

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

type PromoForm = { uppercaseField: string };

export const promoBehavior = defineFormBehavior<PromoForm>(({ model }) => {
  transformValue(model.$.uppercaseField, (value) => (value ?? '').toUpperCase());
});
```

Пример: `transformValue(model.$.uppercaseField, (v) => (v ?? '').toUpperCase())`.

### Несколько трансформаций

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

type ContactForm = { email: string; phone: string; amount: number };

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

  // телефон: только цифры → формат
  transformValue(model.$.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(model.$.amount, (value) => (typeof value === 'number' ? Math.round(value) : value));
});
```

### Как примитив (вне defineFormBehavior)

```typescript
import { transformValue } from '@reformer/core';
const stop = transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
```

### Переиспользуемые трансформеры — обычные функции

Готового набора нет, но легко собрать свои и применять их к любому сигналу:

```typescript
const toUpper = (target: Signal<string>) => transformValue(target, (v) => (v ?? '').toUpperCase());
const trim = (target: Signal<string>) => transformValue(target, (v) => (v ?? '').trim());

// в схеме поведения:
toUpper(model.$.promoCode);
trim(model.$.username);
```

## 53. Anti-patterns

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

// ✅ Guard внутри transformer
transformValue(model.$.field, (v) => (v?.startsWith('prefix-') ? v : `prefix-${v}`));
```

```typescript
// ❌ transformValue ОДНОВРЕМЕННО с syncFields на том же поле
syncFields(model.$.a, model.$.b);
transformValue(model.$.b, (v) => v.toUpperCase()); // взаимные перезаписи

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

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

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

## 54. Troubleshooting

**Q: «Cycle detected» при transformValue.**
A: 99% — неидемпотентный transformer. Проверь `transformer(transformer(x)) === transformer(x)`.

**Q: Трансформация не применяется.**
A: Проверь, что behavior зарегистрирован (`createForm({ behavior })`) / примитив не отписан,
и что форма не пересоздаётся на каждый рендер (используй `useMemo`).

**Q: Каретка input прыгает при наборе.**
A: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)
лучше делать через `<InputMask>` из `@reformer/ui-kit`, а не `transformValue`.

## 55. 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>` для тяжёлого форматирования

## 56. Purpose

`revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
зависимостей. Применяется, когда правило зависит от значений других полей
(`amount <= maxAmount`, `confirmPassword === password`, `initialPayment >= propertyValue * 0.2`).
Под M1 валидация on-demand (`validateFormModel`), поэтому ревалидация выражается явным
колбэком, а не автоматической привязкой к полю.

## 57. API

Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):

```typescript
// примитив: возвращает cleanup
function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void): () => void;

// DSL: cleanup управляется формой
function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void): void;
```

`deps` — массив сигналов-триггеров (`model.$.field`). `revalidate` — колбэк (обычно
`() => validateFormModel(model, schema)`). Вызывается при изменении любой зависимости
(НЕ на инициализации), вне effect-контекста.

## 58. Examples

### Базовый сценарий — перевалидация amount при смене maxAmount

```typescript
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
import { validateFormModel } from '@reformer/core';

export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
  revalidateWhen([model.$.maxAmount], () => {
    void validateFormModel(model, paymentSchema);
  });
});
```

Пример: `revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema))`.

### Несколько триггеров

```typescript
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
import { validateFormModel } from '@reformer/core';

export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) => {
  // initialPayment зависит и от propertyValue, и от loanAmount
  revalidateWhen([model.$.propertyValue, model.$.loanAmount], () => {
    void validateFormModel(model, mortgageSchema);
  });
});
```

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

```typescript
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
import { validateFormModel } from '@reformer/core';

// правило совпадения — cross-field ModelValidator на confirmPassword (см. 03-api-signatures.md)
export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ model }) => {
  // при смене password перевалидируем схему (confirm перепроверится)
  revalidateWhen([model.$.password], () => {
    void validateFormModel(model, registrationSchema);
  });
});
```

### Как примитив (вне defineFormBehavior)

```typescript
import { revalidateWhen, validateFormModel } from '@reformer/core';
const stop = revalidateWhen([model.$.maxAmount], () => void validateFormModel(model, schema));
```

## 59. Anti-patterns

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

// ✅ Триггеры — ДРУГИЕ поля, от которых зависит правило amount
revalidateWhen([model.$.maxAmount, model.$.discount], () => validateFormModel(model, schema));
```

```typescript
// ❌ Правило только через revalidateWhen, но в схеме нет зависимости от триггера
revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
// а валидатор amount — статичный max(1000), не читает maxAmount

// ✅ Сначала cross-field ModelValidator, читающий root, потом revalidateWhen
const amountVsMax: ModelValidator<number, unknown, Form> = (v, _s, root) =>
  v != null && root.maxAmount != null && v > root.maxAmount ? { code: 'tooBig', message: '...' } : null;
// amount: { value: model.$.amount, validators: [amountVsMax] }
revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
```

## 60. Troubleshooting

**Q: Ошибка target не пропадает после изменения триггера.**
A: Проверьте, что (1) правило реально читает значение триггера через `root` (cross-field
`ModelValidator`); (2) в `deps` передан именно сигнал (`model.$.trigger`); (3) колбэк вызывает
`validateFormModel` (роутит ошибки в ноды).

**Q: Как перевалидировать только одно поле, а не всю схему?**
A: Передай в колбэк под-схему только этого поля: `validateFormModel(model, { children: [fieldNode] })`.

## 61. See also

- [03-api-signatures.md](./03-api-signatures.md) — cross-field `ModelValidator` и `validateFormModel`
- [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
- [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения

## 62. Purpose

Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateFormModel`) →
проверить `result.valid` → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
Валидация — чистая функция ДАННЫХ (модели), она же роутит ошибки в ноды формы, поэтому UI
подсветит проблемные поля. `model.reset()` возвращает значения к initial-снимку.

## 63. API

```typescript
// Модель (источник истины значений):
interface ModelApi<T> {
  get(): T;                         // снимок значений — для submit
  set(value: T): void;              // полная установка (все ключи T)
  patch(value: Partial<T>): void;   // частичное слияние
  isDirty(): boolean;               // отличаются ли значения от initial-снимка
  reset(): void;                    // вернуть к initial-снимку
  captureInitial(): void;           // зафиксировать текущие как новый initial
}

// Валидация данных (headless, роутит ошибки в ноды формы):
validateFormModel<T>(model, schema): Promise<{ valid: boolean; errors: Record<string, ValidationError[]> }>

// Нода поля (для точечной работы с UI-состоянием):
form.<field>.setErrors([{ code, message }]);
form.<field>.markAsTouched();
form.<field>.reset();
```

## 64. Examples

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

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

type RegistrationFormData = { username: string; email: string; password: string };

function RegistrationForm() {
  const { model, form, schema } = useMemo(() => {
    const m = createModel<RegistrationFormData>({ username: '', email: '', password: '' });
    const s = buildSchema(m);
    return { model: m, form: createForm({ model: m, schema: s }), schema: s };
  }, []);

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

    // Шаг 1: валидация данных (ошибки автоматически проставятся в ноды → UI подсветит)
    const result = await validateFormModel(model, schema);
    if (!result.valid) return;

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

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

  return (
    <form onSubmit={handleSubmit}>
      <FormField control={form.username} testId="username" />
      <FormField control={form.email} testId="email" />
      <FormField control={form.password} testId="password" />
      <button type="submit">Отправить</button>
    </form>
  );
}
```

### Submit с error-handling и серверными ошибками

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

  const result = await validateFormModel(model, schema);
  if (!result.valid) return;

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

### Reset с подтверждением

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

  const clearJustPassword = () => {
    model.password = ''; // сброс одного поля — прямая запись в модель
  };

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

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

Валидация headless — работает без UI/нод:

```typescript
import { createModel, validateModel } from '@reformer/core';

async function processFormPayload(rawData: Partial<MyForm>) {
  const model = createModel<MyForm>(initialValues);
  model.patch(rawData); // частичный load данных

  const result = await validateModel(model, schema); // без нод — просто данные
  if (!result.valid) return { ok: false, errors: result.errors };
  return { ok: true, data: model.get() };
}
```

## 65. Anti-patterns

```typescript
// ❌ Чтение результата валидации без await
const handleSubmit = (e) => {
  e.preventDefault();
  validateFormModel(model, schema); // не дождались async-валидаторов
  submit(model.get());
};

// ✅ Дождаться и проверить result.valid
const handleSubmit = async (e) => {
  e.preventDefault();
  const result = await validateFormModel(model, schema);
  if (result.valid) submit(model.get());
};
```

```typescript
// ❌ model.reset() до ответа сервера — потеря данных при ошибке
await api.send(model.get());
model.reset();

// ✅ Reset только после успеха
try {
  await api.send(model.get());
  model.reset();
} catch (err) { showError(err); }
```

```typescript
// ❌ Сброс полей по очереди — многословно
model.username = '';
model.email = '';
model.password = '';

// ✅ model.reset() — к initial-снимку одним вызовом
model.reset();
```

## 66. Troubleshooting

**Q: После `reset()` в UI остались старые ошибки.**
A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
`validateFormModel(model, schema)` после reset, либо очисти точечно `form.field.setErrors([])`.

**Q: `reset()` не возвращает данные, загруженные с сервера.**
A: `reset()` возвращает к initial-снимку (значения на момент `createModel`). `set/patch` НЕ
меняют initial. Чтобы сделать загруженные данные новой «точкой отсчёта» — вызови
`model.captureInitial()` после загрузки.

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

## 67. See also

- [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
- [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateFormModel`
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateFormModel`
- [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки

## 68. Purpose

Загрузка формы данными с сервера под M1: (1) **initial values** в `createModel(...)`; (2)
**`model.set` / `model.patch`** для загрузки/обновления значений; (3) **external React-hook**
для full-blown async preload (параллельный fetch заявки + справочников, обработка ошибок,
race-guard через deps). Динамические `componentProps` (опции селектов) обновляются через
`form.field.updateComponentProps({ options })` в `queueMicrotask`, чтобы не пересечься с
реактивными эффектами от `set`/`patch`.

## 69. API

```typescript
// Модель:
model.set(value: T): void;            // полная установка значений (load полного DTO). Не меняет initial.
model.patch(value: Partial<T>): void; // частичное слияние (только переданные ключи). Не меняет initial.
model.reset(): void;                  // вернуть к initial-снимку
model.captureInitial(): void;         // сделать текущие значения новым initial-снимком

// Нода поля/группы:
form.field.updateComponentProps(props: Record<string, unknown>): void; // динамические опции и т.п.
```

Initial values задаются в `createModel(initial)`; `reset()` возвращает к ним. Чтобы загруженные
данные стали новой «точкой отсчёта» для `reset()` — вызови `model.captureInitial()` после load.

## 70. Examples

### Initial values в модели

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

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

const model = createModel<ProfileForm>({ username: '', language: 'ru', marketing: true });
const schema = {
  username: { value: model.$.username, component: Input, componentProps: { label: 'Username' } },
  language: {
    value: model.$.language,
    component: Select,
    componentProps: {
      label: 'Язык',
      options: [
        { value: 'ru', label: 'Русский' },
        { value: 'en', label: 'English' },
      ],
    },
  },
  marketing: { value: model.$.marketing, component: Input, componentProps: { type: 'checkbox' } },
};
const form = createForm({ model, schema });
// model.get() === { username: '', language: 'ru', marketing: true }
// после правок: model.reset() возвращает к этим значениям
```

### Async preload через external hook + model.set

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

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

export function useLoadCreditApplication(
  model: FormModel<CreditApplicationForm>,
  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(),
        ]);
        if (cancelled) return; // race-guard: сменили applicationId / unmount
        if (appResp.status !== 200 || dictsResp.status !== 200) throw new Error('Сервер вернул ошибку');

        // Загрузка значений в модель
        model.set(appResp.data);

        // Динамические componentProps — через queueMicrotask (после реактивных эффектов от set)
        queueMicrotask(() => {
          if (cancelled) return;
          form.registrationAddress.city.updateComponentProps({ options: dictsResp.data.cities });
        });

        setState({ isLoading: false, error: null });
      } catch (err) {
        if (cancelled) return;
        setState({ isLoading: false, error: err instanceof Error ? err.message : 'Ошибка' });
      }
    })();

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

  return state;
}
```

### Preload через behavior — onChange на поле

Загрузка справочника при выборе другого поля (после preload) — через `onChange`:

```typescript
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';

export const addressBehavior = defineFormBehavior<AddressForm>(({ model, form }) => {
  onChange(
    model.$.region,
    async (region, { signal }) => {
      if (!region) { form.city.updateComponentProps({ options: [] }); return; }
      try {
        const cities = await fetchCities(region, { signal });
        form.city.updateComponentProps({ options: cities });
      } catch {
        form.city.updateComponentProps({ options: [] });
      }
    },
    { debounce: 300 }
  );
});
```

## 71. Anti-patterns

```typescript
// ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз
function MyForm() {
  const model = createModel<T>(initial);   // КАЖДЫЙ рендер!
  const form = createForm({ model, schema });
}

// ✅ Стабильные ссылки через useMemo
function MyForm() {
  const { model, form } = useMemo(() => {
    const m = createModel<T>(initial);
    return { model: m, form: createForm({ model: m, schema: buildSchema(m) }) };
  }, []);
}
```

```typescript
// ❌ updateComponentProps синхронно после set → возможный конфликт с реактивными эффектами
model.set(data);
form.region.updateComponentProps({ options: [...] });

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

```typescript
// ❌ Нет race-guard в async useEffect
useEffect(() => { fetchData(id).then((d) => model.set(d)); }, [id]);

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

## 72. Troubleshooting

**Q: После `set`/`patch` не показываются ошибки.**
A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateFormModel(model, schema)`.

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

**Q: При `reset()` теряются данные с сервера.**
A: `reset()` возвращает к initial-снимку из `createModel`, не к последнему `set`. Чтобы «сбросить
к данным с сервера» — после `set` вызови `model.captureInitial()` (новая точка отсчёта).

## 73. See also

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

## 74. 30. TYPE-SAFETY RECIPES

Идиоматичные паттерны, которые держат сгенерированный код без `any` и `as`-кастов под M1.

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

- Модель/форма/валидация/хуки/типы/**примитивы behaviors** — из `@reformer/core`.
- Чистые фабрики валидаторов — из `@reformer/core/validators`.
- Декларативный DSL (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.

```typescript
import {
  createModel,
  createForm,
  validateFormModel,
  type FormModel,
  type FormProxy,
  type ModelSignals,
  type ModelValidator,
} from '@reformer/core';
import { required, min, max, email } from '@reformer/core/validators';
import { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';
```

> **watchField — из `@reformer/core`** (примитив), НЕ из `@reformer/core/behaviors` (там `onChange`).

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

`Record<string, FormValue>` требует index signature. У `interface` её нет неявно; у `type` —
структурно. Объявляй через `type`-alias всё, что попадает в `FormProxy<T>`/`ArrayNode<T>`:
корневую форму, вложенные группы, типы элементов массива.

```typescript
export type PropertyItem = {
  type: 'apartment' | 'house' | 'car';
  description: string;
  estimatedValue: number;
};

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

### Recipe 3 — Схема привязана к сигналам модели

`value` поля — это сигнал модели (`model.$.field`), а не литерал. Тип поля выводится из сигнала.

```typescript
const model = createModel<CreditApplicationForm>(initial);

const schema = {
  loanAmount: { value: model.$.loanAmount, component: Input, validators: [required(), min(50000)] },
  // вложенная группа — builder, принимающий ModelSignals<Sub>
  personalData: personalDataNodes(model.$.personalData),
  // массив — { array, item }
  properties: { array: model.properties, item: propertyItem },
};
```

### Recipe 4 — Cross-field валидаторы: типизированный `ModelValidator`

Правило — `ModelValidator<TField, TScope, TRoot>`. `root` типизирован формой, соседние поля
читаются без `as`:

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

const initialPaymentVsProperty: ModelValidator<number, unknown, CreditApplicationForm> = (
  _value,
  _scope,
  root
) =>
  root.initialPayment && root.propertyValue && root.initialPayment > root.propertyValue
    ? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }
    : null;

// в схеме:
initialPayment: { value: model.$.initialPayment, component: Input, validators: [initialPaymentVsProperty] };
```

### Recipe 5 — `compute` читает модель напрямую (без аннотаций)

`compute(target, () => …)` читает value-модель (`model.field`) — типы полей выводятся из типа
модели, `as`-касты не нужны:

```typescript
compute(model.$.monthlyPayment, () =>
  annuityMonthly(model.loanAmount ?? 0, model.loanTerm ?? 0, model.interestRate ?? 0)
);

// nested reads — тоже напрямую
compute(model.$.fullName, () =>
  [model.personalData.firstName, model.personalData.lastName].filter(Boolean).join(' ')
);
```

### Recipe 6 — `null` vs `undefined` для опциональных полей

Оба работают. `null` — конвенция «пользователь очистил поле». Встроенные валидаторы
(`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`, `maxAge`) пропускают
пустые значения — guard `if (value != null)` не нужен.

```typescript
export type CreditForm = {
  loanAmount: number | null;   // min(model.$.loanAmount, 50000) — ок
  loanPurpose: string | null;  // minLength — ок
  birthDate: string | null;    // minAge — ок
};
```

### Recipe 7 — Нода поля для кастомных компонентов

`useFormControl(control)` типизируется по `FieldNode<T>`. В props компонента используй `FieldNode<T>`:

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

type MyFieldProps<T> = { control: FieldNode<T> };
function MyField<T>({ control }: MyFieldProps<T>) {
  const { value, errors, disabled } = useFormControl(control);
  // ...
}
```

### Recipe 8 — Вынос правил в именованные функции

Cross-field и field-правила — именованные `ModelValidator`, схема остаётся плоской:

```typescript
const validateAdultAge: ModelValidator<string> = (value) => {
  if (!value) return null;
  const age = new Date().getFullYear() - new Date(value).getFullYear();
  return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;
};

const schema = {
  birthDate: { value: model.$.birthDate, component: Input, validators: [validateAdultAge] },
};
```

### Anti-patterns to avoid

- `import { computeFrom } from '@reformer/core/behaviors'` для примитива, вызываемого вне
  `defineFormBehavior` → примитив живёт в `@reformer/core` (возвращает cleanup).
- `interface MyForm { ... }` для form-shape → см. Recipe 2.
- `as`-касты значений полей внутри `compute` → читай `model.field` напрямую (Recipe 5).
- строковые пути / `(form) => ...` в behaviors → это удалённый API, используй сигналы (`model.$.x`).

## 75. 31. ASYNC VALIDATOR

Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
async-валидатор это `ModelValidator`, возвращающий `Promise<ValidationError | null>`. Он
исполняется движком `validateFormModel`/`validateModel` (async-задачи прогоняются параллельно
через `Promise.all`).

```ts
import { createModel, createForm, validateFormModel, type ModelValidator } from '@reformer/core';
import { required, email } from '@reformer/core/validators';
import { Input } from '@reformer/ui-kit';

// async-валидатор: (value, scope, root) => Promise<ValidationError | null>
const checkEmailUnique: ModelValidator<string> = async (value) => {
  if (!value) return null; // пусто = валидно (sync `required` отдельно)
  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 model = createModel<{ email: string }>({ email: '' });
const schema = {
  email: {
    value: model.$.email,
    component: Input,
    // sync-фабрики и async-валидатор в одном массиве validators
    validators: [required(), email(), checkEmailUnique],
  },
};
const form = createForm({ model, schema });
```

### Как это исполняется

1. `validateFormModel(model, schema)` собирает field-задачи и прогоняет их валидаторы.
2. Для каждого поля валидаторы выполняются по порядку; async-валидаторы `await`-ятся.
3. `validateModelSync(model, schema)` — синхронный вариант: async-валидаторы **пропускаются**
   (для мгновенных проверок без сети).
4. Ошибки роутятся в ноды формы (`form.field.errors`), UI подсвечивает поле.

### Отдельное поле `asyncValidators`

В схеме можно разделить sync и async: `validators: [...]` и `asyncValidators: [...]`. Оба типа
поддерживаются `FieldConfig`. На практике удобнее держать всё в `validators` — движок сам
различает sync/async по возвращаемому `Promise`.

### UI integration

`FormField` из `@reformer/ui-kit` показывает индикатор проверки, пока идёт async-валидация
(`useFormControl(...).pending === true`):

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

### Debounce и отмена

- Валидация запускается on-demand (на submit / шаг / через `revalidateWhen`), а не на каждый
  keystroke — отдельный `debounce` в валидаторе обычно не нужен.
- Если нужно дебаунсить дорогой async-валидатор относительно частых изменений, вешай его через
  `revalidateWhen([...], () => validateFormModel(...))` и оборачивай запуск в собственный debounce.
- Cross-field async — обычный `ModelValidator`, читающий соседние поля через `root`.

### See also

- [27-revalidate-when.md](27-revalidate-when.md) — перезапуск валидации по триггерам
- [29-async-preload.md](29-async-preload.md) — async preload данных при init формы
- [03-api-signatures.md](03-api-signatures.md) — `ModelValidator` и `validateFormModel`

## 76. 32. ASYNC OPTIONS LOADING

Для динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,
`carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +
`form.field.updateComponentProps({ options })`. `onChange` даёт debounce и AbortSignal из коробки.

```ts
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';

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

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

export const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
  onChange(
    model.$.registrationAddress.region,
    async (region, { signal }) => {
      // сбросить зависимое поле при смене source
      model.registrationAddress.city = '';

      if (!region) {
        form.registrationAddress.city.updateComponentProps({ options: [] });
        return;
      }

      // (опционально) loading-состояние
      form.registrationAddress.city.updateComponentProps({ loading: true, options: [] });

      try {
        const options = await fetchCitiesByRegion(region, { signal }); // отмена устаревших
        form.registrationAddress.city.updateComponentProps({ loading: false, options });
      } catch (e) {
        if ((e as Error).name === 'AbortError') return; // устаревший запрос — молча выходим
        form.registrationAddress.city.updateComponentProps({ loading: false, options: [] });
      }
    },
    { debounce: 300 }
  );
});
```

### Lifecycle

1. `onChange` подписан на изменения `model.$.region`.
2. Debounce 300 мс (не fetch на каждое нажатие клавиши).
3. Async `fetchCitiesByRegion(region, { signal })` — `signal` отменяет устаревший запрос при
   следующей смене значения (защита от race).
4. Результат — `updateComponentProps({ options })` на target-поле.
5. UI (Select / Combobox) обновляется через сигнал `componentProps`.

### Переиспользуемый оператор

Как в монорепо — собираешь свой оператор поверх `onChange`:

```ts
import { onChange } from '@reformer/core/behaviors';
import type { ReadonlySignal } from '@reformer/core/behaviors';

function loadOptionsOn<TValue, TOption>(
  source: ReadonlySignal<TValue>,
  target: { updateComponentProps(p: Record<string, unknown>): void; reset?: () => void },
  fetcher: (value: TValue, opts?: { signal?: AbortSignal }) => Promise<TOption[]>,
  options: { debounce?: number; resetTarget?: boolean } = {}
): void {
  const { debounce = 300, resetTarget = false } = options;
  onChange(
    source,
    async (value, { signal }) => {
      if (resetTarget) target.reset?.();
      if (!value) { target.updateComponentProps({ options: [] }); return; }
      try {
        const data = await fetcher(value, { signal });
        target.updateComponentProps({ options: data });
      } catch {
        target.updateComponentProps({ options: [] });
      }
    },
    { debounce }
  );
}

// Usage:
loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: true });
```

### Common patterns

- **Debounce 300–500 мс** — баланс UX и rate-limit.
- **Reset target value** — при смене source очисти зависимое поле (`model.city = ''`), чтобы не
  остался устаревший выбор.
- **Loading state** — `updateComponentProps({ loading: true })` пока идёт fetch (компонент должен поддержать).
- **Cancellation** — `onChange` даёт `{ signal }` (AbortSignal); передавай его в `fetch` и
  игнорируй `AbortError`.

### Initial load (preload at form mount)

Для загрузки опций при инициализации формы — external hook + `model.set` + `updateComponentProps`
(см. [29-async-preload.md](29-async-preload.md)). `onChange` не срабатывает на init (по умолчанию
`immediate: false`); для запуска сразу передай `{ immediate: true }`.

### See also

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

## 77. API Reference

_Auto-generated from JSDoc on public exports._

### AnyFunction

**Kind:** `type`

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

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

_Source: src/form/types/index.ts_

### ArrayConfig

**Kind:** `interface`

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

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

_Source: src/form/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;

  /**
   * Флаг отключения массива.
   * `true` когда массив отключён (`ArrayNode.disable()`), в том числе через
   * распространение disable от родительской группы.
   *
   * Используйте для отключения UI-действий добавления/удаления, когда массив
   * структурно неизменяем.
   *
   * @example
   * ```tsx
   * const { disabled } = useFormControl(itemsArray);
   *
   * return <button disabled={disabled} onClick={() => control.push()}>Add</button>;
   * ```
   */
  disabled: 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/form/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/form/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/form/types/index.ts_

### AsyncValidator

**Kind:** `type`

Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
{@link Validator}, но возвращает `Promise`.

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

**Deprecated:** Осиротевший остаток удалённого оператора `validateAsync` (Ф7). Рантаймом не
потребляется — живой async-путь узла поля использует `AsyncValidatorFn` `(value, { signal })`.
Экспортируется только ради обратной совместимости.

_Source: src/form/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/form/types/contracts.ts_

### AsyncValidatorOptions

**Kind:** `interface`

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

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

_Source: src/form/types/contracts.ts_

### BehaviorCleanup

**Kind:** `type`

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

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

_Source: src/state/behaviors-value.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/state/behaviors-value.ts_

### ConditionFn

**Kind:** `type`

Функция-предикат `(value) => boolean`.

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

**Deprecated:** Осиротевший остаток удалённого оператора `applyWhen` (Ф7). Рантаймом не
потребляется; экспортируется только ради обратной совместимости.

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

### ConfigWithSchema

**Kind:** `interface`

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

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

_Source: src/form/types/index.ts_

### ConfigWithValue

**Kind:** `interface`

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

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

_Source: src/form/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/state/behaviors-value.ts_

### createForm

**Kind:** `function`

Создать форму из {@link FormModel} + единой схемы (архитектура M1, рекомендуемый путь).

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

**Parameters:**
- `args` — - Модель данных, единая схема (component/componentProps/validators) и (опционально) поведение

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

**Examples:**

Архитектура M1: `createModel` + единая схема + `createForm({ model, schema })`
```typescript
import { createModel, createForm, validateFormModel } from '@reformer/core';
import { required, email, minLength } from '@reformer/core/validators';

interface UserForm {
email: string;
password: string;
}

const model = createModel<UserForm>({ email: '', password: '' });

const schema = {
children: [
{
value: model.$.email,
component: Input,
validators: [required(), email()],
},
{
value: model.$.password,
component: Input,
validators: [required(), minLength(8)],
},
],
};

const form = createForm<UserForm>({ model, schema });

// TypeScript знает о полях (нода привязана к сигналу модели):
form.email.setValue('test@mail.com');

// Валидация всей модели по схеме (sync + async):
const { valid } = await validateFormModel(model, schema);
```

_Source: src/form/create-form.ts_

### createFormFromModel

**Kind:** `function`

Собрать форму из {@link FormModel} и единой схемы (низкоуровневая фабрика архитектуры M1).

Значения принадлежат модели (источник истины), ноды формы держат UI/валидационное состояние и
ссылаются на сигналы модели по идентичности (`node.value === model.$.path`). Обходит структуру
модели, привязывает конфиг поля (component/componentProps/validators) из схемы, материализует
top-level массивы как {@link ModelArrayNode}, заполняет реестр сигнал→нода (для `enableWhen`/
роутинга ошибок) и, при наличии, запускает декларативное поведение (cleanup живёт на форме).

Обычно вызывается неявно через {@link createForm} с аргументом `{ model, schema }` — прямой вызов
нужен редко (например, для построения формы элемента массива).

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

**Parameters:**
- `args` — - Модель, единая схема и (опционально) декларативное поведение {@link CreateFormFromModelArgs}

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

**Examples:**

Форма из модели + схемы (эквивалент `createForm({ model, schema })`)
```typescript
import { createModel, createFormFromModel } from '@reformer/core';

interface Form {
email: string;
profile: { name: string; age: number };
}

const model = createModel<Form>({ email: '', profile: { name: '', age: 0 } });
const schema = {
component: Section,
children: [
{ value: model.$.email, component: Input, validators: [required] },
// вложенная группа: `model.$.profile.name` (≡ под-модель `model.profile.$.name` — тот же сигнал)
{ value: model.$.profile.name, component: Input },
],
};

const form = createFormFromModel<Form>({ model, schema });

// Двусторонняя связь нода ↔ модель:
form.email.setValue('user@mail.com');
console.log(model.email); // 'user@mail.com'
```

**See also:**
- {@link createForm} - основная фабрика (диспетчеризует сюда при аргументе `{ model, schema }`)

_Source: src/form/create-form.ts_

### CreateFormFromModelArgs

**Kind:** `interface`

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

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

_Source: src/form/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; profile: { name: string }; tags: string[] }>({
  email: '',
  profile: { name: '' },
  tags: [],
});
model.email = 'a@b.c';
model.$.email.value;          // 'a@b.c' (сигнал)
// вложенная объект-группа — под-модель FormModel (value-доступ + `.$` + API):
model.profile.name = 'Ada';   // value-запись
model.$.profile.name.value;   // 'Ada' (сигнал; ≡ model.profile.$.name у под-модели)
model.profile.get();          // { name: 'Ada' }
model.tags.push('x');
model.get();                  // { email: 'a@b.c', profile: { name: 'Ada' }, tags: ['x'] }
```

_Source: src/state/form-model.ts_

### disableWhen

**Kind:** `function`

Условное выключение поля (инверсия {@link enableWhen}). Резолвит ноду по сигналу-цели через реестр
сигнал→нода и вызывает `disable()`, когда `condition` истинно (+`reset()` при `resetOnDisable`).
`condition` реактивен (читает свои сигналы модели).

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

**Parameters:**
- `target` — Сигнал-цель поля (`model.$.<path>`).
- `condition` — Реактивное условие; при `true` поле выключается.

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

**Examples:**

```typescript
// Поле скидки недоступно, пока не выбран промо-тариф
disableWhen(model.$.discount, () => model.plan !== 'promo', { resetOnDisable: true });
```

_Source: src/form/behaviors-node.ts_

### email

**Kind:** `function`

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

Проверяет по упрощённому regex `^[^\s@]+@[^\s@]+\.[^\s@]+$`. Пустые значения
(`''`/`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).

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

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для строкового поля

**Examples:**

Проверка формата email
```typescript
import { required, email } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
email: {
value: model.$.email,
component: Input,
validators: [required(), email({ message: 'Введите корректный email' })],
},
```

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

### enableWhen

**Kind:** `function`

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

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

⚠️ Если то же поле одновременно является целью `compute`/`computeFrom`, обязательно ограничь
compute тем же условием (`when`), что и это `enableWhen`. Механизмы не согласованы: `enableWhen`
работает со статус-машиной ноды (enable/disable/reset), а `compute` пишет `target.value` напрямую
в сигнал модели и проверяет только собственный `options.when` — состояние `disabled` он НЕ смотрит.
Поэтому при выключенном поле любое изменение зависимости compute молча заново заполнит его (а с
`resetOnDisable` — перезапишет только что сброшенное значение), и так как `getValue()`/`submit`
включают disabled-поля, мусор утечёт в payload. Это аналог правила #13 гайда add-behavior,
которое сегодня сформулировано только для `copyFrom`; для `compute` то же ограничение обязательно.

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

**Examples:**

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

// Поле-цель compute, которое ТАКЖЕ в resetOnDisable enableWhen: compute обязан нести тот же when,
// иначе initialPayment заново заполнится в выключенном состоянии и утечёт в submit.
compute(model.$.initialPayment, () => model.propertyValue * 0.2, {
  when: () => model.loanType === 'mortgage',
});
```

_Source: src/form/behaviors-node.ts_

### ErrorFilterOptions

**Kind:** `interface`

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

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

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

  /** Фильтр по параметрам ошибки */
  params?: Record<string, FormValue>;

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

_Source: src/form/types/contracts.ts_

### ErrorStrategy

**Kind:** `enum`

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

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

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

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

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

_Source: src/form/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/form/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}
   *   />
   * );
   * ```
   */
  componentProps: Record<string, unknown>;

  /**
   * Флаг изменения поля.
   * `true` когда значение поля отличается от начального.
   *
   * Реактивный аналог `control.dirty` — паритет с {@link ArrayControlState.dirty}.
   *
   * @example
   * ```tsx
   * const { dirty } = useFormControl(field);
   *
   * return dirty ? <span className="badge">Изменено</span> : null;
   * ```
   */
  dirty: boolean;
}
```

**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/form/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/form/nodes/field-node.ts_

### FieldPathSegment

**Kind:** `type`

Тип для путей к полям (field paths)
Используется в навигации по полям вместо any

**Signature:**
```typescript
export type FieldPathSegment = {
  key: string;
  index?: number;
};
```

_Source: src/form/types/index.ts_

### FieldStatus

**Kind:** `type`

Статус поля формы

**Signature:**
```typescript
export type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
```

_Source: src/form/types/contracts.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/form/types/form-proxy.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/form/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/form/error-handler.ts_

### FormModel

**Kind:** `type`

FormModel — под-модель объекта `T`: value-доступ ({@link ModelObject}) + `.$`-сигналы + {@link ModelApi}
(get/set/patch/isDirty/reset/signalAt). Вложенные объекты-группы модели — тоже {@link FormModel}
(доступны как `model.<group>`), поэтому `model.<group>.$.<field>` эквивалентно `model.$.<group>.<field>`.

**Signature:**
```typescript
export type FormModel<T> = ModelObject<T> & ModelApi<T>;
```

_Source: src/state/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/form/nodes/form-node.ts_

### FormProxy

**Kind:** `type`

Комбинированный тип для GroupNode с Proxy доступом к полям

Объединяет методы и свойства GroupNode с типизированными полями формы.
Это позволяет использовать как API GroupNode, так и прямой доступ к полям.

**Signature:**
```typescript
export type FormProxy<T> = GroupNode<T> &
  // Исключаем имена, совпадающие с членами GroupNode: для них `form.<name>` возвращает член узла,
  // а не поле. Оставлять их в полевом пространстве — значит пересекать, напр., ReadonlySignal<FieldStatus>
  // с FieldNode<string> и молча компилировать невозможный тип. Escape-hatch — `form.$` ниже.
  Omit<FormControlsProxy<T>, keyof GroupNode<T>> & {
    /**
     * Escape-hatch пространство имён «controls»: типобезопасный доступ ко ВСЕМ полям формы
     * по имени, включая поля, чьи имена совпадают с членами {@link GroupNode}
     * (`value`/`status`/`id`/`errors`/…), недостижимые через `form.<name>`.
     *
     * @example
     * ```typescript
     * // модель: { status: string; email: string }
     * form.status;      // ReadonlySignal<FieldStatus> — агрегат GroupNode (не поле!)
     * form.$.status;    // FieldNode<string> — поле пользователя
     * form.$.status.setValue('active');
     * form.$.email;     // FieldNode<string> — так же доступны и незатенённые поля
     * ```
     */
    readonly $: 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/form/types/form-proxy.ts_

### FormSchema

**Kind:** `type`

**Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
(или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
- `T[] -> [FormSchema<T>]` (массив с одним элементом)
- `object -> FormSchema<T>` (группа)
- `primitive -> FieldConfig<T>` (поле)

Использует NonNullable для корректной обработки опциональных полей.

⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
(ключи = поля данных), а не узел дерева.

**Signature:**
```typescript
export type FormSchema<T> = {
  // Листовые позиции используют NonEmptyFieldConfig: пустой `{}` (частая опечатка) отклоняется
  // на этапе компиляции, но каждое свойство FieldConfig по отдельности остаётся опциональным.
  [K in keyof T]: NonNullable<T[K]> extends string | number | boolean
    ? NonEmptyFieldConfig<T[K]>
    : NonNullable<T[K]> extends Array<infer U>
      ? U extends string | number | boolean
        ? NonEmptyFieldConfig<T[K]>
        : U extends Date | File | Blob | AnyFunction
          ? NonEmptyFieldConfig<T[K]>
          : [FormSchema<U>]
      : NonNullable<T[K]> extends Date | File | Blob | AnyFunction
        ? NonEmptyFieldConfig<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/form/types/deep-schema.ts_

### FormSchemaNode

**Kind:** `interface`

Узел единой схемы M1 — дерево, обходимое `createForm({ model, schema })`,
`validateModel`/`validateFormModel` и рендерерами.

Узел совмещает несколько ролей (различаются рантаймом по форме):
 - **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
 - **массив** — `{ array: model.<path>, item(itemModel) }`;
 - **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
 - **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).

Индексная сигнатура (`[key: string]: unknown`) отражает свободный рекурсивный обход: под-узлы
допустимы под любым ключом. Известные поля типизированы (даёт автокомплит и проверку их типов).

**Signature:**
```typescript
export interface FormSchemaNode {
  /**
   * «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма
   * зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до
   * `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
   */
  value?: unknown;
  /** UI-компонент. Опционален: core-часть работает без UI (значение/валидация). */
  component?: ComponentType<any>;
  /** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
  componentProps?: Record<string, unknown>;
  validators?: SchemaValidator[];
  asyncValidators?: SchemaValidator[];
  updateOn?: 'change' | 'blur' | 'submit';
  disabled?: boolean;
  /** Задержка (мс) перед запуском асинхронной валидации. */
  debounce?: number;
  /** Идентификатор узла (для wizard/tabs/renderBehavior). */
  selector?: string;
  testId?: string;
  /** Дочерние узлы (даёт контекстную типизацию вложенным литералам — value/validators/when). */
  children?: readonly FormSchemaNode[];
  /** Условие включения поддерева (branch-узел `{ when, children }`). */
  when?: (scope: any, root: any) => boolean;
  /** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */
  array?: SchemaArrayControl;
  /** Схема элемента массива: под-модель элемента → узел поддерева. */
  item?: (itemModel: any) => FormSchemaNode;
  /**
   * Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,
   * либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`
   * схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.
   */
  initialValue?: unknown;
  /** Свободная вложенность: record-of-fields и произвольные под-узлы. */
  [key: string]: unknown;
}
```

_Source: src/form/types/schema-node.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/form/status-machine.ts_

### FormSubmitter

**Kind:** `class`

FormSubmitter - управляет процессом отправки формы

**Signature:**
```typescript
export class FormSubmitter<T extends object> {
  /** Внутренний сигнал состояния отправки */ /* … */ }
```

**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/form/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/form/types/contracts.ts_

### futureDate

**Kind:** `function`

Фабрика валидатора, проверяющего что дата не в прошлом.

Дата не должна быть раньше сегодняшнего дня (сравнение по нормализованным датам).
Пустые и невалидные даты пропускаются (используйте {@link required} и {@link isDate}).

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

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)

**Examples:**

Дата не в прошлом
```typescript
import { required, futureDate } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
appointmentDate: {
value: model.$.appointmentDate,
component: DatePicker,
validators: [required(), futureDate({ message: 'Дата записи должна быть в будущем' })],
},
```

_Source: src/form/validation/validators/future-date.ts_

### getNodeForSignal

**Kind:** `function`

Найти ноду формы по сигналу модели.

Используется state-операциями behavior (`enableWhen`/`disableWhen`) и роутингом ошибок
валидации в ноды. Возвращает `undefined`, если форма ещё не построена или поле не
материализовано в этой форме (например, элемент массива, который строится per-item).

**Signature:**
```typescript
export function getNodeForSignal(signal: Signal<any>): FormNode<any> | undefined
```

**Parameters:**
- `signal` — - Сигнал значения из {@link FormModel}

**Returns:** Нода формы для этого поля или `undefined`

**Examples:**

Роутинг ошибок валидации в ноду поля
```typescript
import { getNodeForSignal } from '@reformer/core';

const sig = model.$.email;
getNodeForSignal(sig)?.setErrors([{ code: 'required', message: 'Обязательно' }]);
```

**See also:**
- {@link registerSignalNode} - регистрация связи сигнал→нода

_Source: src/form/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/form/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/form/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/form/types/index.ts_

### integer

**Kind:** `function`

Фабрика валидатора, проверяющего что число — целое.

Пустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).

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

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для числового поля

**Examples:**

Проверка целого числа
```typescript
import { required, integer } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
count: {
value: model.$.count,
component: Input,
validators: [required(), integer({ message: 'Должно быть целым числом' })],
},
```

_Source: src/form/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<object>
```

**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/form/type-guards.ts_

### isDate

**Kind:** `function`

Фабрика валидатора, проверяющего что значение — валидная дата.

Принимает `Date` или строку, парсимую в дату. Пустые значения (`''`/`null`/`undefined`)
пропускаются (используйте {@link required} для обязательности).

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

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)

**Examples:**

Проверка валидности даты
```typescript
import { required, isDate } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
eventDate: {
value: model.$.eventDate,
component: DatePicker,
validators: [required(), isDate({ message: 'Введите корректную дату' })],
},
```

_Source: src/form/validation/validators/is-date.ts_

### isDerived

**Kind:** `function`

Производный ли сигнал (помечен через {@link markDerived}).

Читается bulk-сеттерами модели/группы, чтобы не затирать вычисляемые (`computeFrom`) поля
значениями из payload.

**Signature:**
```typescript
export function isDerived(signal: Signal<any>): boolean
```

**Parameters:**
- `signal` — - Сигнал значения из {@link FormModel}

**Returns:** `true`, если сигнал помечен производным

**Examples:**

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

if (!isDerived(model.$.total)) {
  model.$.total.value = payload.total; // писать в payload-значение только для «ручных» полей
}
```

**See also:**
- {@link markDerived} - пометить сигнал производным

_Source: src/state/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/form/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/form/type-guards.ts_

### isGroupNode

**Kind:** `function`

Проверить, является ли значение GroupNode (объект с вложенными полями)

GroupNode представляет объект с вложенными полями формы: имеет навигацию по полям
(`getFieldByPath`/`fields`) и НЕ имеет array-методов (`items`/`push`/`removeAt`).

**Signature:**
```typescript
export function isGroupNode(value: unknown): value is GroupNode<object>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является GroupNode

**Examples:**

```typescript
if (isGroupNode(node)) {
  node.getFieldByPath('user.email'); //  OK
}
```

_Source: src/form/type-guards.ts_

### isNumber

**Kind:** `function`

Фабрика валидатора, проверяющего что значение — конечное число (не NaN, не строка).

Пустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для
обязательности). В отличие от других number-валидаторов, **не** пропускает не-числа
и `NaN` — это его задача.

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

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для числового поля

**Examples:**

Проверка, что значение — число
```typescript
import { required, isNumber } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
amount: {
value: model.$.amount,
component: Input,
validators: [required(), isNumber({ message: 'Введите число' })],
},
```

_Source: src/form/validation/validators/is-number.ts_

### markDerived

**Kind:** `function`

Пометить сигнал производным (вычисляемым).

Вызывается операторами {@link computeFrom}/`compute` для их сигнала-цели. После этого
bulk-сеттеры (`model.set`/`model.patch`, `patchValue`/`setValue`) пропускают это поле, чтобы
значение из payload не затирало вычисляемое. Прикладной код напрямую обычно не вызывает.

Идемпотентна на уровне флага: повторные вызовы наращивают счётчик ссылок, чтобы каждый оператор,
пишущий в этот сигнал, был сбалансирован своим {@link unmarkDerived} при dispose.

**Signature:**
```typescript
export function markDerived(signal: Signal<any>): void
```

**Parameters:**
- `signal` — - Сигнал значения из {@link FormModel}, которым владеет compute

**Examples:**

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

// Помечаем поле total как вычисляемое — bulk-set его не перезапишет
markDerived(model.$.total);
```

**See also:**
- {@link isDerived} - проверка пометки
- {@link unmarkDerived} - снять пометку при dispose оператора

_Source: src/state/derived-registry.ts_

### max

**Kind:** `function`

Фабрика валидатора максимального числового значения.

Пустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).

**Signature:**
```typescript
export function max<TForm = unknown, TField extends number | null | undefined = number>(
  maxValue: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Parameters:**
- `maxValue` — - Максимально допустимое значение (включительно)
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадают `max` и `actual`.

**Returns:** Чистый валидатор {@link Validator} для числового поля

**Examples:**

Максимальное значение числового поля
```typescript
import { required, max } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
quantity: { value: model.$.quantity, component: Input, validators: [max(100)] },
discount: {
value: model.$.discount,
component: Input,
validators: [required(), max(50, { message: 'Не более 50%' })],
},
```

_Source: src/form/validation/validators/max.ts_

### maxAge

**Kind:** `function`

Фабрика валидатора максимального возраста (по дате рождения).

Возраст вычисляется по дате рождения относительно сегодняшнего дня. Пустые и невалидные
даты пропускаются (используйте {@link required} и {@link isDate}).

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

**Parameters:**
- `maxAgeValue` — - Максимально допустимый возраст (в полных годах)
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадают `maxAge` и `currentAge`.

**Returns:** Чистый валидатор {@link Validator} для поля даты рождения (`string | Date`)

**Examples:**

Максимальный возраст
```typescript
import { maxAge } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
birthDate: {
value: model.$.birthDate,
component: DatePicker,
validators: [maxAge(100, { message: 'Проверьте дату рождения' })],
},
```

_Source: src/form/validation/validators/max-age.ts_

### maxDate

**Kind:** `function`

Фабрика валидатора максимальной даты (включительно).

Сравнение по нормализованным датам (время обнуляется). Пустые и невалидные даты
пропускаются (используйте {@link required} и {@link isDate}).

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

**Parameters:**
- `maxDateValue` — - Максимально допустимая дата (включительно)
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадает `maxDate`.

**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)

**Examples:**

Максимальная дата
```typescript
import { maxDate } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
birthDate: {
value: model.$.birthDate,
component: DatePicker,
validators: [maxDate(new Date(), { message: 'Дата не может быть в будущем' })],
},
```

_Source: src/form/validation/validators/max-date.ts_

### maxLength

**Kind:** `function`

Фабрика валидатора максимальной длины строки или массива.

Работает со строкой или массивом (проверяется `value.length`). Пустые значения
(`null`/`undefined`/`''`) и значения без числового `length` пропускаются
(используйте {@link required} для обязательности).

**Signature:**
```typescript
export function maxLength<TForm = unknown, TField = unknown>(
  maxLen: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Parameters:**
- `maxLen` — - Максимально допустимая длина (включительно)
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадают `maxLength` и `actualLength`.

**Returns:** Чистый валидатор {@link Validator} для строки или массива

**Examples:**

Максимальная длина строки
```typescript
import { maxLength } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
name: { value: model.$.name, component: Input, validators: [maxLength(50)] },
bio: {
value: model.$.bio,
component: Textarea,
validators: [maxLength(500, { message: 'Максимум 500 символов' })],
},
```

_Source: src/form/validation/validators/max-length.ts_

### min

**Kind:** `function`

Фабрика валидатора минимального числового значения.

Пустые значения (`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).

**Signature:**
```typescript
export function min<TForm = unknown, TField extends number | null | undefined = number>(
  minValue: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Parameters:**
- `minValue` — - Минимально допустимое значение (включительно)
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадают `min` и `actual`.

**Returns:** Чистый валидатор {@link Validator} для числового поля

**Examples:**

Минимальное значение числового поля
```typescript
import { required, min } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
age: { value: model.$.age, component: Input, validators: [min(18)] },
quantity: {
value: model.$.quantity,
component: Input,
validators: [required(), min(1, { message: 'Минимум 1' })],
},
```

_Source: src/form/validation/validators/min.ts_

### minAge

**Kind:** `function`

Фабрика валидатора минимального возраста (по дате рождения).

Возраст вычисляется по дате рождения относительно сегодняшнего дня. Пустые и невалидные
даты пропускаются (используйте {@link required} и {@link isDate}).

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

**Parameters:**
- `minAgeValue` — - Минимально допустимый возраст (в полных годах)
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадают `minAge` и `currentAge`.

**Returns:** Чистый валидатор {@link Validator} для поля даты рождения (`string | Date`)

**Examples:**

Минимальный возраст
```typescript
import { required, minAge } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
birthDate: {
value: model.$.birthDate,
component: DatePicker,
validators: [required(), minAge(18, { message: 'Вам должно быть не менее 18 лет' })],
},
```

_Source: src/form/validation/validators/min-age.ts_

### minDate

**Kind:** `function`

Фабрика валидатора минимальной даты (включительно).

Сравнение по нормализованным датам (время обнуляется). Пустые и невалидные даты
пропускаются (используйте {@link required} и {@link isDate}).

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

**Parameters:**
- `minDateValue` — - Минимально допустимая дата (включительно)
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадает `minDate`.

**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)

**Examples:**

Минимальная дата
```typescript
import { required, minDate } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
startDate: {
value: model.$.startDate,
component: DatePicker,
validators: [required(), minDate(new Date(), { message: 'Дата не раньше сегодня' })],
},
```

_Source: src/form/validation/validators/min-date.ts_

### minLength

**Kind:** `function`

Фабрика валидатора минимальной длины строки или массива.

Работает со строкой или массивом (проверяется `value.length`). Пустые значения
(`null`/`undefined`/`''`) и значения без числового `length` пропускаются
(используйте {@link required} для обязательности).

**Signature:**
```typescript
export function minLength<TForm = unknown, TField = unknown>(
  minLen: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Parameters:**
- `minLen` — - Минимально допустимая длина (включительно)
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадают `minLength` и `actualLength`.

**Returns:** Чистый валидатор {@link Validator} для строки или массива

**Examples:**

Минимальная длина строки
```typescript
import { required, minLength } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
name: { value: model.$.name, component: Input, validators: [minLength(2)] },
password: {
value: model.$.password,
component: Input,
validators: [required(), minLength(8, { message: 'Минимум 8 символов' })],
},
```

_Source: src/form/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;
  /**
   * Полная установка значений: принимает объект целиком (все ключи `T`) и записывает их в модель.
   * Производными полями (цели `compute`) владеет compute — их значения из payload игнорируются.
   * Не меняет initial-снимок. Для частичного обновления (только переданные ключи) — {@link ModelApi.patch}.
   */
  set(value: T): void;
  /**
   * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
   * отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
   */
  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/state/types.ts_

### ModelArray

**Kind:** `interface`

Реактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;
`map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.

**Signature:**
```typescript
export interface ModelArray<U> {
  /**
   * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
   * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
   */
  readonly __path: string;
  /** Реактивная длина. */
  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;
  /** Элемент по индексу: объект → под-модель, массив → {@link ModelArray}, лист → значение. */
  at(
    index: number
  ): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
  /** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
  map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
  /** Итерация по элементам (см. {@link ModelArrayItem}). */
  forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
  /** Снимок массива значений (без подписки). */
  toArray(): U[];
  /** Индексный value-доступ. */
  [index: number]: ModelValue<U>;
}
```

_Source: src/state/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/form/nodes/model-array-node.ts_

### ModelArrayNode

**Kind:** `class`

Узел массива, делегирующий данные массиву {@link FormModel} (архитектура M1).

В отличие от {@link ArrayNode} (владеет элементами сам), `ModelArrayNode` НЕ владеет данными:
массив принадлежит модели, а узел держит per-item формы элементов (привязанные к сигналам
под-моделей) и синхронизирует их с длиной массива модели. Мутации (`push`/`removeAt`/`move`/…)
делегируются массиву модели; per-item формы кэшируются по идентичности под-модели, поэтому при
reorder/повторном рендере не пересоздаются (состояние и валидация сохраняются). Реализует тот же
контракт, что ждут секции массива и `useFormControl` (`length`/`value`/`valid`/`errors`/`at`/`push`/…).

Обычно создаётся не напрямую, а `createForm({ model, schema })`: когда в схеме встречается узел
массива `{ array: model.<field>, item: (item) => itemSchema }`, форма материализует его как
`ModelArrayNode` и кладёт под `form.<field>` (совместим с `FormArraySection`).

**Signature:**
```typescript
export class ModelArrayNode<T extends object> extends FormNode<T[]> { /* … */ }
```

**Examples:**

Массив как часть формы (через createForm)
```typescript
const model = createModel<{ rows: { name: string; qty: number }[] }>({ rows: [] });

const rowItem = (item: FormModel<{ name: string; qty: number }>) => ({
name: { value: item.$.name, component: Input },
qty: { value: item.$.qty, component: Input },
});

const form = createForm({
model,
schema: {
children: [{ array: model.rows, item: rowItem }],
},
});

const rows = form.rows as unknown as ModelArrayNode<{ name: string; qty: number }>;
rows.push({ name: 'A', qty: 1 });   // мутация уезжает в model.rows
rows.at(0)?.name.setValue('B');     // правка поля элемента доезжает в под-модель
rows.length.value;                  // 1 (реактивная длина)
```

_Source: src/form/nodes/model-array-node.ts_

### ModelObject

**Kind:** `type`

Карта value-полей объекта (value-половина {@link FormModel}): поля доступны как обычные свойства
(чтение реактивно внутри `effect`/`computed`, запись — присваиванием). Вложенные объекты-поля
резолвятся в под-модели {@link FormModel} (см. {@link ModelValue}), массивы — в {@link ModelArray}.

**Signature:**
```typescript
export type ModelObject<T> = {
  [K in keyof T]: ModelValue<T[K]>;
};
```

_Source: src/state/types.ts_

### ModelSignals

**Kind:** `type`

Дерево сигналов (escape-hatch `model.$`): листья → {@link PathAwareSignal},
объекты → вложенное дерево, массивы → индексируемое дерево под-сигналов.

**Signature:**
```typescript
export type ModelSignals<T> = {
  [K in keyof T]: ModelSignalNode<T[K]>;
};
```

_Source: src/state/types.ts_

### ModelValidationResult

**Kind:** `interface`

Результат валидации: ошибки по пути поля.

**Signature:**
```typescript
export interface ModelValidationResult {
  valid: boolean;
  /** Ключ — путь поля (`'coBorrowers.0.relationship'`). Только поля с ошибками. */
  errors: Record<string, ValidationError[]>;
}
```

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

### ModelValidator

**Kind:** `type`

Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
массива или корень); `root` — корневая модель.

Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
`FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).

**Signature:**
```typescript
export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
  value: TValue,
  model: TModel,
  root: TRoot
) => ValidationError | null | Promise<ValidationError | null>;
```

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

### ModelValue

**Kind:** `type`

Значение поля в value-доступе модели:
- массив → {@link ModelArray}
- спец-объект (Date/File/Blob) → как есть
- объект → под-модель {@link FormModel} (value-доступ + `.$`-сигналы + API get/set/patch/…);
  промоутится рантаймом (`makeFormModel`); сигналы идентичны `model.$.<path>`
- примитив → значение

**Signature:**
```typescript
export type ModelValue<V> =
  NonNullable<V> extends ReadonlyArray<infer U>
    ? ModelArray<U>
    : NonNullable<V> extends Opaque
      ? V
      : NonNullable<V> extends object
        ? FormModel<NonNullable<V>>
        : V;
```

_Source: src/state/types.ts_

### multipleOf

**Kind:** `function`

Фабрика валидатора, проверяющего что число кратно заданному.

Пустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).

**Signature:**
```typescript
export function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(
  divisor: number,
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Parameters:**
- `divisor` — - Делитель: значение должно быть кратно ему
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадает `multipleOf` (делитель).

**Returns:** Чистый валидатор {@link Validator} для числового поля

**Examples:**

Проверка кратности
```typescript
import { multipleOf } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
rating: {
value: model.$.rating,
component: Input,
validators: [multipleOf(0.5, { message: 'Только шаг 0.5' })],
},
```

_Source: src/form/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 factory = new NodeFactory();
const node = factory.createNode({ value: '', component: Input }); // → FieldNode<string>
```

_Source: src/form/factories/node-factory.ts_

### nonNegative

**Kind:** `function`

Фабрика валидатора, проверяющего что число неотрицательное (`≥ 0`).

Пустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).

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

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для числового поля

**Examples:**

Проверка неотрицательности
```typescript
import { nonNegative } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
balance: {
value: model.$.balance,
component: Input,
validators: [nonNegative({ message: 'Баланс не может быть отрицательным' })],
},
```

_Source: src/form/validation/validators/non-negative.ts_

### nonZero

**Kind:** `function`

Фабрика валидатора, проверяющего что число не равно нулю.

Пустые значения и не-числа пропускаются (используйте {@link required} и {@link isNumber}).

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

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для числового поля

**Examples:**

Проверка «не ноль»
```typescript
import { nonZero } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
divisor: {
value: model.$.divisor,
component: Input,
validators: [nonZero({ message: 'Не может быть нулём' })],
},
```

_Source: src/form/validation/validators/non-zero.ts_

### pastDate

**Kind:** `function`

Фабрика валидатора, проверяющего что дата не в будущем.

Дата не должна быть позже сегодняшнего дня (сравнение по нормализованным датам).
Пустые и невалидные даты пропускаются (используйте {@link required} и {@link isDate}).

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

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для поля даты (`string | Date`)

**Examples:**

Дата не в будущем
```typescript
import { required, pastDate } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
birthDate: {
value: model.$.birthDate,
component: DatePicker,
validators: [required(), pastDate({ message: 'Дата рождения не может быть в будущем' })],
},
```

_Source: src/form/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/state/types.ts_

### pattern

**Kind:** `function`

Фабрика валидатора регулярного выражения.

Пустые значения (`''`/`null`/`undefined`) пропускаются (используйте {@link required}
для обязательности).

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

**Parameters:**
- `regex` — - Регулярное выражение для проверки значения
- `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
попадает `pattern` (строка-источник regex).

**Returns:** Чистый валидатор {@link Validator} для строкового поля

**Examples:**

Проверка по регулярному выражению
```typescript
import { required, pattern } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
name: {
value: model.$.name,
component: Input,
validators: [pattern(/^[a-zA-Zа-яА-Я]+$/, { message: 'Только буквы' })],
},
phone: {
value: model.$.phone,
component: InputMask,
validators: [
required(),
pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message: 'Формат +7 (999) 123-45-67' }),
],
},
```

_Source: src/form/validation/validators/pattern.ts_

### phone

**Kind:** `function`

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

Проверяет значение по regex выбранного {@link PhoneFormat}. Пустые значения
(`''`/`null`/`undefined`) пропускаются (используйте {@link required} для обязательности).

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

**Parameters:**
- `options` — - Опции валидатора {@link PhoneValidatorOptions}. В `params` ошибки
автоматически попадает выбранный `format`.

**Returns:** Чистый валидатор {@link Validator} для строкового поля

**Examples:**

Проверка номера телефона
```typescript
import { required, phone } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
phone: {
value: model.$.phone,
component: Input,
validators: [required(), phone({ format: 'ru' })],
},
```

_Source: src/form/validation/validators/phone.ts_

### PhoneFormat

**Kind:** `type`

Формат проверки номера телефона для валидатора {@link phone}.

- `international` — международный формат E.164 (`+?[1-9]\d{1,14}`);
- `ru` — российские номера (`+7`/`7`/`8`, коды `4`/`8`/`9`, с разделителями);
- `us` — североамериканские номера (NANP);
- `any` — свободный формат: цифры, скобки и разделители (по умолчанию).

**Signature:**
```typescript
export type PhoneFormat = 'international' | 'ru' | 'us' | 'any';
```

_Source: src/form/validation/validators/phone.ts_

### registerSignalNode

**Kind:** `function`

Связать сигнал модели с его нодой формы.

Вызывается движком при сборке формы (`createForm`/{@link createFormFromModel}) для каждого
листового поля. Прикладной код обычно этот реестр не заполняет напрямую.

**Signature:**
```typescript
export function registerSignalNode(signal: Signal<any>, node: FormNode<any>): void
```

**Parameters:**
- `signal` — - Сигнал значения из {@link FormModel} (ручка поля, `model.$.path`)
- `node` — - Нода формы, отвечающая за это поле

**Examples:**

Привязка листовых полей при построении формы
```typescript
import { registerSignalNode } from '@reformer/core';

const sig = model.signalAt('profile.email');
const node = group.getFieldByPath('profile.email');
if (sig && node) registerSignalNode(sig, node);
```

**See also:**
- {@link getNodeForSignal} - обратный поиск ноды по сигналу

_Source: src/form/signal-node-registry.ts_

### required

**Kind:** `function`

Фабрика валидатора обязательного поля.

Возвращает чистую функцию-валидатор `(value, control, root)`. Передаётся в `validate()`.

Пустыми считаются: `null`, `undefined`, `''` (пустая строка), `[]` (пустой массив —
обязательный multi-select / FormArray без выбранных элементов).
Для boolean полей требуется значение `true`.

**Signature:**
```typescript
export function required<TForm = unknown, TField = unknown>(
  options?: ValidateOptions
): Validator<TForm, TField>
```

**Parameters:**
- `options` — - Опции валидатора ({@link ValidateOptions}): `message`, `params`

**Returns:** Чистый валидатор {@link Validator} для поля схемы

**Examples:**

Обязательные поля в схеме формы
```typescript
import { required } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
email: { value: model.$.email, component: Input, validators: [required()] },
phone: {
value: model.$.phone,
component: Input,
validators: [required({ message: 'Укажите номер телефона' })],
},
agreeToTerms: {
value: model.$.agreeToTerms,
component: Checkbox,
validators: [required({ message: 'Необходимо принять условия' })],
},
```

_Source: src/form/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/state/behaviors-value.ts_

### ResourceLoadResult

**Kind:** `type`

Тип для результатов загрузки ресурсов

**Signature:**
```typescript
export type ResourceLoadResult = unknown;
```

_Source: src/form/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/state/behaviors-value.ts_

### runOutsideEffect

**Kind:** `function`

Выполняет функцию вне контекста effect (отложенная запись на микротаск).

Сбои fn (синхронный throw и async-rejection) маршрутизируются dev-логгером (logDeferError), а не
всплывают неперехваченными. Возвращает отменитель: вызов до срабатывания микротаска отменяет запись
(liveness-охрана — не писать против снесённого узла при dispose в том же тике).

**Signature:**
```typescript
export function runOutsideEffect(fn: () => void | Promise<void>): () => void
```

**Parameters:**
- `fn` — - Функция для выполнения

**Returns:** Отменитель ещё не выполненной записи (no-op, если запись уже выполнена)

**Examples:**

```typescript
effect(() => {
  const value = signal.value;
  const cancel = runOutsideEffect(() => {
    otherSignal.value = transform(value);
  });
  onDispose(cancel); // отменить ожидающую запись, если узел снесут в этом же тике
});
```

_Source: src/state/safe-effect.ts_

### safeCallback

**Kind:** `function`

Создает callback, который выполняется вне контекста effect

Откладывает вызов через {@link runOutsideEffect} — то есть получает маршрутизацию ошибок и
обработку async-rejection «бесплатно».

**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/state/safe-effect.ts_

### safeDebouncedCallback

**Kind:** `function`

Создает версию callback с поддержкой debounce, безопасную для effect

`withDebounce` уже откладывает вызов (таймер) — этого достаточно для выхода из effect-контекста,
поэтому дополнительного `queueMicrotask` нет (устранён двойной defer). Сбои callback (в т.ч.
async-rejection) маршрутизируются dev-логгером (logDeferError).

**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/state/safe-effect.ts_

### SchemaArrayControl

**Kind:** `interface`

Минимальный контракт реактивного массива модели ({@link FormSchemaNode.array}).
Совпадает по форме с рантайм-фасадом `model.<array>` (см. `ModelArray`); рендерерский
`RenderModelArrayControl` — его расширение (добавляет `move`).

**Signature:**
```typescript
export interface SchemaArrayControl {
  /** Путь массива в модели (dot-нотация) — нужен для резолва узла массива. */
  readonly __path: string;
  /** Реактивная длина. */
  readonly length: number;
  at(index: number): unknown;
  push(item: unknown): void;
  removeAt(index: number): void;
}
```

_Source: src/form/types/schema-node.ts_

### SchemaValidator

**Kind:** `type`

Валидатор поля схемы. Узел хранит validators гетерогенно: движок `validate*` вызывает их как
`(value, scope, root)`, но сюда кладут любой контракт — `ValidatorFn` (value),
`Validator` (value, control, root) или `ModelValidator` (value, model, root). Поэтому тип
намеренно широкий по параметрам.

**Signature:**
```typescript
export type SchemaValidator = (
  value: any,
  ...rest: any[]
) => ValidationError | null | Promise<ValidationError | null>;
```

_Source: src/form/types/schema-node.ts_

### SetValueOptions

**Kind:** `interface`

Опции для setValue

**Signature:**
```typescript
export interface SetValueOptions {
  /** Не вызывать событие изменения (не триггерить валидацию) */
  emitEvent?: boolean;
  /** Обновить только этот узел, не распространять на родителей */
  onlySelf?: boolean;
}
```

_Source: src/form/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/form/status-machine.ts_

### SubmitOptions

**Kind:** `interface`

Опции для submit

**Signature:**
```typescript
export interface SubmitOptions {
  /** Пропустить валидацию перед submit */
  skipValidation?: boolean;
  /** Пропустить markAsTouched перед submit */
  skipTouch?: boolean;
}
```

_Source: src/form/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/form/form-submitter.ts_

### SubmittableForm

**Kind:** `interface`

Интерфейс формы для FormSubmitter
Минимальный контракт для работы с любой формой

**Signature:**
```typescript
export interface SubmittableForm<T extends object> {
  /** Пометить все поля как touched */
  markAsTouched(): void;
  /** Валидировать форму */
  validate(): Promise<boolean>;
  /** Получить значения формы */
  getValue(): T;
}
```

_Source: src/form/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/state/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/state/behaviors-value.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/state/behaviors-value.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/form/unique-id.ts_

### UnknownCallback

**Kind:** `type`

Тип для коллбэков и обработчиков событий
Используется вместо (...args: any[]) => any

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

_Source: src/form/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/form/types/contracts.ts_

### UnknownRecord

**Kind:** `type`

Тип для Record с unknown значениями
Используется вместо инлайнового `Record<string, unknown>`

**Signature:**
```typescript
export type UnknownRecord = Record<string, unknown>;
```

_Source: src/form/types/index.ts_

### unmarkDerived

**Kind:** `function`

Снять пометку производного сигнала (обратная операция к {@link markDerived}).

Вызывается из очистки (`onDispose`) оператора `compute`/`computeFrom`, когда поведение снимается,
но модель/сигнал продолжают жить (динамическая перекоммутация: удалить под-схему → сохранить модель
→ bulk-set). Без этого сигнал остаётся помеченным навсегда, и bulk-сеттеры навсегда пропускают
поле, которое больше ничем не вычисляется.

Учитывает счётчик ссылок: пометка снимается только когда снят последний владелец. Вызов на
непомеченном сигнале — безопасный no-op.

**Signature:**
```typescript
export function unmarkDerived(signal: Signal<any>): void
```

**Parameters:**
- `signal` — - Сигнал значения из {@link FormModel}, ранее помеченный {@link markDerived}

**Examples:**

```typescript
import { markDerived, unmarkDerived } from '@reformer/core';

markDerived(model.$.total);
onDispose(() => unmarkDerived(model.$.total)); // при снятии compute снова разрешаем bulk-set
```

**See also:**
- {@link markDerived} - пометить сигнал производным

_Source: src/state/derived-registry.ts_

### url

**Kind:** `function`

Фабрика валидатора URL.

Пустые значения (`''`/`null`/`undefined`) пропускаются (используйте {@link required}
для обязательности). При `requireProtocol` протокол обязателен; `allowedProtocols`
дополнительно ограничивает набор допустимых протоколов.

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

**Parameters:**
- `options` — - Опции валидатора {@link UrlValidatorOptions}

**Returns:** Чистый валидатор {@link Validator} для строкового поля

**Examples:**

Проверка URL
```typescript
import { required, url } from '@reformer/core/validators';

// Внутри FieldConfig схемы формы:
website: {
value: model.$.website,
component: Input,
validators: [required(), url({ message: 'Введите корректный URL' })],
},
// Требовать протокол и ограничить схему только https:
homepage: {
value: model.$.homepage,
component: Input,
validators: [url({ requireProtocol: true, allowedProtocols: ['https'] })],
},
```

_Source: src/form/validation/validators/url.ts_

### useArrayLength

**Kind:** `function`

React-хук для подписки только на длину массива.

Оптимизированная версия {@link useFormControl} для ArrayNode, которая
подписывается только на сигнал `length`. Компонент не будет ре-рендериться
при изменении значений вложенных полей.

**Signature:**
```typescript
export function useArrayLength<T extends object>(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/form/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<object> | undefined
): FieldControlState<FormValue> | ArrayControlState<object>
```

**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/form/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: number;
price: number;
}

// Хук вызывается на уровне компонента-строки (не в цикле — Rules of Hooks).
// Доступ к полям элемента — через Proxy: item.quantity, item.price.
function OrderRow({ item }: { item: FormProxy<OrderItem> }) {
const quantity = useFormControlValue(item.quantity);
const price = useFormControlValue(item.price);

return <span>Итого: ${(quantity * price).toFixed(2)}</span>;
}

function OrderList({ items }: { items: ArrayNode<OrderItem> }) {
return (
<div>
{items.map((item, index) => (
<OrderRow key={index} item={item} />
))}
</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/form/hooks/useFormControlValue.ts_

### ValidateAsyncOptions

**Kind:** `interface`

**Signature:**
```typescript
export interface ValidateAsyncOptions extends ValidateOptions {
  /** Задержка перед выполнением валидации (в мс). */
  debounce?: number;
}
```

**Deprecated:** Осиротевший остаток удалённого оператора `validateAsync` (Ф7): опция `debounce`
подключалась тем оператором, которого больше нет. Рантаймом не потребляется; экспортируется
только ради обратной совместимости.

_Source: src/form/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: FormSchemaNode
): Promise<ModelValidationResult>
```

**Parameters:**
- `model` — - Модель данных ({@link FormModel}) — источник значений полей.
- `schema` — - Единая схема формы (та же, что передавалась в `createForm`).

**Returns:** 

**Examples:**

Валидация перед submit (ошибки показываются в UI автоматически)
```typescript
const form = createForm({ model, schema });
form.touchAll();
const res = await validateFormModel(model, schema);
if (res.valid) await api.save(model.get());
```

_Source: src/form/validate-model.ts_

### validateModel

**Kind:** `function`

Headless-валидация данных (sync + async). Работает без UI/нод.

**Signature:**
```typescript
export async function validateModel<T>(
  model: FormModel<T>,
  schema: FormSchemaNode
): Promise<ModelValidationResult>
```

**Examples:**

```typescript
const res = await validateModel(model, schema);
if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
```

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

### validateModelSync

**Kind:** `function`

Синхронная headless-валидация данных. Асинхронные валидаторы пропускаются.

**Signature:**
```typescript
export function validateModelSync<T>(
  model: FormModel<T>,
  schema: FormSchemaNode
): ModelValidationResult
```

**Parameters:**
- `model` — - Модель данных ({@link FormModel}), из сигналов которой читаются значения полей.
- `schema` — - Единая схема формы (дерево узлов с `value`/`validators`, `{ when, children }`,
секциями массивов). Обходится один раз для сбора активных полей.

**Returns:** 

**Examples:**

Быстрая синхронная проверка (например, для gate «можно ли перейти на след. шаг»)
```typescript
const res = validateModelSync(model, schema);
if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
```

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

### ValidateOptions

**Kind:** `interface`

Опции валидатора-фабрики (`required()`/`pattern()`/…). Передаются вторым (или последним)
аргументом в фабрику и попадают в возвращаемую {@link ValidationError}.

**Signature:**
```typescript
export interface ValidateOptions {
  /** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
   * резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */
  message?: string;
  /** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
  params?: Record<string, FormValue>;
}
```

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

### ValidationError

**Kind:** `interface`

Ошибка валидации

**Signature:**
```typescript
export interface ValidationError {
  code: string;
  message: string;
  params?: Record<string, FormValue>;
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
  severity?: 'error' | 'warning';
}
```

_Source: src/form/types/contracts.ts_

### Validator

**Kind:** `type`

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

Сигнатура зеркалит то, что реально вызывает движок M1
(`validateModel`/`validateFormModel`/`validateModelSync`): `validator(value, scope, root)`.
- `value` — значение поля (`TField`);
- `scope` — ближайшая scope-**модель** (под-модель элемента массива или корень). Из `TForm`/`TField`
  её тип не выводится, поэтому `unknown` — потребитель сужает сам (для типизированного scope
  используйте {@link ModelValidator}`<TField, TScope, TForm>`);
- `root` — корневая модель формы `FormModel<TForm>` (реактивный value-proxy: `root.field` читает
  значение). Возвращает `ValidationError` либо `null`. Не знает про реестр валидации.

Совместим с полем `validators` узла схемы (см. `SchemaValidator`) — там же лежат `ModelValidator`
и `ValidatorFn`. Встроенные фабрики (`required()`/`email()`/…) возвращают `(value) => …` и
дополнительные аргументы игнорируют.

**Signature:**
```typescript
export type Validator<TForm, TField> = (
  value: TField,
  scope: unknown,
  root: FormModel<TForm>
) => ValidationError | null;
```

**Examples:**

Кастомный валидатор в массиве `validators` поля схемы
```typescript
const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
if (value < 18) return { code: 'tooYoung', message: '18+' };
// root — FormModel<MyForm>: root.someOtherField читается как значение
return null;
};

// Фабрики и кастомные валидаторы кладутся в `validators: [...]` поля:
const schema = {
children: [
{ value: model.$.age, component: Input, validators: [required(), isAdult] },
],
};
```

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

### ValidatorFn

**Kind:** `type`

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

**Signature:**
```typescript
export type ValidatorFn<T = FormValue> = (value: T) => ValidationError | null;
```

_Source: src/form/types/contracts.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/state/behaviors-value.ts_

### WithBehaviorSchema

**Kind:** `interface`

Интерфейс для узлов с методом applyBehaviorSchema

**Signature:**
```typescript
export interface WithBehaviorSchema {
  applyBehaviorSchema(schemaFn: unknown): void;
}
```

_Source: src/form/types/index.ts_

### WithValidationSchema

**Kind:** `interface`

Интерфейс для узлов с методом applyValidationSchema

**Signature:**
```typescript
export interface WithValidationSchema {
  applyValidationSchema(schemaFn: unknown): void;
}
```

_Source: src/form/types/index.ts_
