# 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: 1.1.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
- 19-reading-values.md — reading-values
- 20-compute-vs-watch.md — compute-vs-watch
- 21-array-operations.md — array-operations
- 22-cycle-detection.md — cycle-detection
- 23-copy-from.md — copyFrom — Копирование значений между полями
- 24-sync-fields.md — syncFields — Двусторонняя синхронизация полей
- 25-reset-when.md — resetWhen — Условный сброс полей
- 26-transform-value.md — transformValue — Автоматическая трансформация значений
- 27-revalidate-when.md — revalidateWhen — Перевалидация полей по триггерам
- 28-submit-and-reset.md — Submit и Reset — Жизненный цикл отправки формы
- 29-async-preload.md — Async Preload — Загрузка начальных значений и динамических справочников
- 30-type-safety-recipes.md — type-safety-recipes
- API Reference (auto-generated from JSDoc)

## 1. 1. API Reference

### Imports (CRITICALLY IMPORTANT)

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

### Type Values

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

### React Hooks Comparison (CRITICALLY IMPORTANT)

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

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

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

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

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

## 2. 1.5 QUICK START - Minimal Working Form

```typescript
import { createForm, useFormControl } from '@reformer/core';
import { required, email } from '@reformer/core/validators';
import type { FormProxy } from '@reformer/core';

// 1. Define form type
interface ContactForm {
  name: string;
  email: string;
}

// 2. Create form schema with validation
const form = createForm<ContactForm>({
  form: {
    name: { value: '', component: Input },
    email: { value: '', component: Input },
  },
  validation: (path) => {
    required(path.name, { message: 'Name is required' });
    required(path.email, { message: 'Email is required' });
    email(path.email, { message: 'Invalid email format' });
  },
});

// 3. Use in React component
function ContactFormComponent() {
  const nameCtrl = useFormControl(form.name);
  const emailCtrl = useFormControl(form.email);

  const handleSubmit = async () => {
    await form.submit((values) => {
      console.log('Form submitted:', values);
    });
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
      <div>
        <input
          value={nameCtrl.value}
          onChange={(e) => form.name.setValue(e.target.value)}
          disabled={nameCtrl.disabled}
        />
        {nameCtrl.errors.map((err) => <span key={err.code}>{err.message}</span>)}
      </div>
      <div>
        <input
          value={emailCtrl.value}
          onChange={(e) => form.email.setValue(e.target.value)}
          disabled={emailCtrl.disabled}
        />
        {emailCtrl.errors.map((err) => <span key={err.code}>{err.message}</span>)}
      </div>
      <button type="submit">Send</button>
    </form>
  );
}

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

function FormStep({ form }: FormStepProps) {
  // Access form fields directly
  const { value } = useFormControl(form.name);
  return <div>Name: {value}</div>;
}
```

## 3. 2. API SIGNATURES

### Validators

```typescript
// Basic validators
required(path, options?: { message?: string })
min(path, value: number, options?: { message?: string })
max(path, value: number, options?: { message?: string })
minLength(path, length: number, options?: { message?: string })
maxLength(path, length: number, options?: { message?: string })
email(path, options?: { message?: string })

// Additional validators
pattern(path, regex: RegExp, options?: { message?: string })
url(path, options?: { message?: string })
phone(path, options?: { message?: string; format?: PhoneFormat })
number(path, options?: { message?: string })
date(path, options?: { message?: string; minAge?: number; maxAge?: number; noFuture?: boolean; noPast?: boolean })

// Custom validators
validate(path, validator: (value, ctx) => ValidationError | null)
validateAsync(path, validator: async (value, ctx) => ValidationError | null)
validateTree(validator: (ctx) => ValidationError | null, options?: { targetField?: string })

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

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

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

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

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

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

// Array validators
notEmpty(path, options?: { message?: string })
validateItems(arrayPath, itemValidatorsFn: (itemPath) => void)
```

### Behaviors

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

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

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

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

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

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

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

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

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

## 4. 3. COMMON PATTERNS

### Conditional Fields with Auto-Reset

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

### Computed Field from Nested to Root Level

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

### Validation callback canonical shape

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

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

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

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

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

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

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

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

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

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

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

### Type-Safe useFormControl

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

### Validation Priority (IMPORTANT)

**Always prefer built-in validators over custom ones:**

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

// 2. GOOD: Use validate() only when no built-in validator exists
validate(path.customField, (value, ctx) => {
  // Custom logic that can't be expressed with built-in validators
  if (customCondition(value)) {
    return { code: 'custom', message: 'Custom error' };
  }
  return null;
});

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

## 5. TYPED schema generic + extracted callback rules

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

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

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

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

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

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

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

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

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

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

**Extract module-level when**:

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

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

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

Reference patterns: `complex-multy-step-form/schemas/credit-application-behavior.ts` and `mcp-credit-application-v10/schema.ts` (Compute helpers section).

## 6. 4. COMMON MISTAKES

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

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

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

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

### useFormControlValue (CRITICAL)

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

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

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

### Reading Field Values in BehaviorContext (CRITICAL)

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

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

### Validators

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

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

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

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

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

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

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

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

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

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

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

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

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

## 7. 5. TROUBLESHOOTING

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

## 8. Import Patterns

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

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

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

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

### Constraint to remember when typing nested forms

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

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

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

## 9. 7. FORM TYPE DEFINITION

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

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

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

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

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

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

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

### FieldConfig Interface

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

### Primitive Fields

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

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

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

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

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

### Nested Objects

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

### Arrays (Tuple Format)

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

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

### WRONG - This will NOT compile

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

### createForm API

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

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

### createForm Returns a Proxy

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

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

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

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

## 11. 9. ARRAY SCHEMA FORMAT

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### Array Item as Sub-Form

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

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

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

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

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

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

### Required Options for async watchField:

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

## 13. 11. ARRAY CLEANUP PATTERN

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

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

## 14. 12. MULTI-STEP FORM VALIDATION

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

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

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

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

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

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

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

  setCurrentStep(currentStep + 1);
};

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

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

### Multi-Step Component Example

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

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

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

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

## 15. 13. EXTENDED COMMON MISTAKES

### Behavior Composition (Cycle Error)

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

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

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

### Infinite Loop in watchField

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

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

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

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

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

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

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

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

### validateTree Typing

```typescript
// WRONG - implicit any
validateTree((ctx) => { ... });

// CORRECT - explicit typing
validateTree((ctx: { form: MyForm }) => {
  if (ctx.form.field1 > ctx.form.field2) {
    return { code: 'error', message: 'Invalid' };
  }
  return null;
});
```

## 16. 14. PROJECT STRUCTURE (COLOCATION)

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

### Key Files

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

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

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

### Scaling

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

## 17. 14.5 UI COMPONENT PATTERNS

ReFormer does NOT provide UI components - you create them yourself or use a UI library.

### Generic FormField Component

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

interface FormFieldProps<T> {
  control: FieldNode<T>;
  label?: string;
  type?: 'text' | 'email' | 'number' | 'password';
  placeholder?: string;
}

function FormField<T extends string | number>({
  control,
  label,
  type = 'text',
  placeholder
}: FormFieldProps<T>) {
  const { value, errors, disabled, touched } = useFormControl(control);
  const showError = touched && errors.length > 0;

  return (
    <div className="form-field">
      {label && <label>{label}</label>}
      <input
        type={type}
        value={value ?? ''}
        onChange={(e) => {
          const val = type === 'number'
            ? Number(e.target.value) as T
            : e.target.value as T;
          control.setValue(val);
        }}
        onBlur={() => control.markAsTouched()}
        disabled={disabled}
        placeholder={placeholder}
        className={showError ? 'error' : ''}
      />
      {showError && (
        <span className="error-message">{errors[0].message}</span>
      )}
    </div>
  );
}

// Usage
<FormField control={form.email} label="Email" type="email" />
<FormField control={form.age} label="Age" type="number" />
```

### FormField for Select

```tsx
interface SelectFieldProps<T extends string> {
  control: FieldNode<T>;
  label?: string;
  options: Array<{ value: T; label: string }>;
}

function SelectField<T extends string>({ control, label, options }: SelectFieldProps<T>) {
  const { value, errors, disabled, touched } = useFormControl(control);

  return (
    <div className="form-field">
      {label && <label>{label}</label>}
      <select
        value={value}
        onChange={(e) => control.setValue(e.target.value as T)}
        disabled={disabled}
      >
        {options.map((opt) => (
          <option key={opt.value} value={opt.value}>
            {opt.label}
          </option>
        ))}
      </select>
      {touched && errors[0] && <span className="error-message">{errors[0].message}</span>}
    </div>
  );
}
```

### Integration with UI Libraries

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

function ShadcnFormField({ control, label }: FormFieldProps<string>) {
  const { value, errors, disabled } = useFormControl(control);

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

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

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

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

### Common Import Errors

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

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

### FormSchema Common Mistakes

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

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

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

### Why .value.value?

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

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

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

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

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

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

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

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

### Reading Nested Values in watchField

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

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

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

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

## 20. 17. COMPUTE FROM vs WATCH FIELD

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

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

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

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

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

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

### watchField - Any Level

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

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

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

### Cross-level write API

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

````

### Rule of Thumb

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

### Stage-pattern for chained computeds

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

### Multiple triggers, one cascade

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

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

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

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

## 21. 18. ARRAY OPERATIONS

### Array Access - CRITICAL

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

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

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

### Array Methods

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

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

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

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

### Rendering Arrays

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

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

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

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

### Array Cross-Validation

```typescript
// Validate uniqueness across array items
validateTree(
  (ctx: { form: MyForm }) => {
    const items = ctx.form.items;
    const names = items.map((item) => item.name.value.value);
    const uniqueNames = new Set(names);

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

// Validate sum of percentages
validateTree(
  (ctx: { form: MyForm }) => {
    const items = ctx.form.items;
    const totalPercent = items.reduce((sum, item) => sum + (item.percentage.value.value || 0), 0);

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

## 22. Cycle Detection Prevention Checklist

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

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

---

## 23. Cycle Detected Error

### Problem

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

### Root Cause

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

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

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

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

### Solution

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

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

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

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

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

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

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

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

### Prefer Built-in Behaviors

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

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

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

### Key Rules

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

### Other Watchers

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

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

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

## 24. Purpose

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

## 25. API

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

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

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

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

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

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

## 26. Examples

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## 27. Anti-patterns

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

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

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

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

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

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

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

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

## 28. Troubleshooting

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

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

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

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

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

## 29. See also

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

## 30. Purpose

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

## 31. API

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

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

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

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

## 32. Examples

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## 33. Anti-patterns

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

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

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

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

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

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

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

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

## 34. Troubleshooting

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

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

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

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

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

## 35. See also

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

## 36. Purpose

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

## 37. API

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

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

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

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

## 38. Examples

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## 39. Anti-patterns

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

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

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

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

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

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

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

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

## 40. Troubleshooting

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

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

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

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

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

## 41. See also

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

## 42. Purpose

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

## 43. API

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

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

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

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

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

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

## 44. Examples

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

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

interface PromoForm {
  uppercaseField: string;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## 45. Anti-patterns

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

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

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

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

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

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

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

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

## 46. Troubleshooting

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

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

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

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

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

## 47. See also

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

## 48. Purpose

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

## 49. API

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

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

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

## 50. Examples

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## 51. Anti-patterns

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

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

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

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

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

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

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

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

## 52. Troubleshooting

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

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

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

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

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

## 53. See also

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

## 54. Purpose

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

## 55. API

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

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

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

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

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

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

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

## 56. Examples

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  if (!isValid) {
    return;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

## 57. Anti-patterns

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

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

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

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

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

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

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

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

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

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

## 58. Troubleshooting

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

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

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

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

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

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

## 59. See also

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

## 60. Purpose

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

## 61. API

```typescript
// FormProxy / GroupNode:

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

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

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

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

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

## 62. Examples

### Initial values в схеме

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

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

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

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

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

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

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

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

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

    let cancelled = false;

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

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

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

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

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

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

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

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

  return state;
}
```

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

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

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

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

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

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

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

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

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

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

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

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

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

## 63. Anti-patterns

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

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

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

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

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

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

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

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

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

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

## 64. Troubleshooting

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

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

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

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

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

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

## 65. See also

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

## 66. 30. TYPE-SAFETY RECIPES

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### Anti-patterns to avoid

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

## 67. API Reference

_Auto-generated from JSDoc on public exports._

### AbstractRegistry

**Kind:** `class`

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

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

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

**Examples:**

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

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

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

### AnyFunction

**Kind:** `type`

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

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

_Source: src/core/types/index.ts_

### apply

**Kind:** `function`

Применить validation схему к вложенному полю или полям

Поддерживает:
- Одно поле или массив полей
- Одну схему или массив схем
- Все комбинации (поле + схема, поле + схемы, поля + схема, поля + схемы)

**Signature:**
```typescript
export function apply<TForm, TField>(
  fields:
    | FieldPathNode<TForm, TField>
    | Array<FieldPathNode<TForm, TField> | undefined>
    | undefined,
  validationSchemas: ValidationSchemaFn<TField> | Array<ValidationSchemaFn<TField>>
): void;
```

**Parameters:**
- `fields` — - Одно поле или массив полей
- `validationSchemas` — - Одна схема или массив схем

**Examples:**

```typescript
// Одна схема к одному полю
apply(path.registrationAddress, addressValidation);

// Одна схема к нескольким полям
apply([path.homeAddress, path.workAddress], addressValidation);

// Несколько схем к одному полю
apply(path.email, [emailValidation, uniqueEmailValidation]);

// Несколько схем к нескольким полям
apply(
  [path.email, path.confirmEmail],
  [emailValidation, matchValidation]
);
```

_Source: src/core/validation/core/apply.ts_

### applyWhen

**Kind:** `function`

Условное применение behavior схем (аналог applyWhen из validation API)

⚠️ ВАЖНО: Эта функция НЕ создаёт новые behaviors при каждом изменении условия!
Вместо этого behaviors регистрируются ОДИН РАЗ при первом вызове и затем
просто не выполняются, если условие не выполнено.

Это отличается от старой реализации, которая создавала утечку памяти,
регистрируя behaviors при каждом изменении conditionField.

**Signature:**
```typescript
export function applyWhen<TForm extends FormFields, TValue extends FormValue>(
  conditionField: FieldPathNode<TForm, TValue> | undefined,
  condition: (value: TValue) => boolean,
  callback: (path: FieldPath<TForm>) => void
): void
```

**Parameters:**
- `conditionField` — - Поле для проверки условия
- `condition` — - Функция проверки условия
- `callback` — - Callback для применения behavior схем

**Examples:**

```typescript
// Применить addressBehavior только когда sameAsRegistration === false
applyWhen(
  path.sameAsRegistration,
  (value) => value === false,
  (path) => {
    apply(path.residenceAddress, addressBehavior);
  }
);

// Или с прямым использованием path
applyWhen(
  path.hasProperty,
  (value) => value === true,
  (path) => {
    apply(path.properties, propertyBehavior);
    // Можно применить несколько схем
    apply([path.properties, path.items], arrayBehavior);
  }
);
```

_Source: src/core/behavior/compose-behavior.ts_

### ArrayConfig

**Kind:** `interface`

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

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

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

### ArrayControlState

**Kind:** `interface`

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

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

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

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

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

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

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

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

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

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

**Examples:**

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

interface Props {
control: ArrayNode<Phone>;
}

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

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

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

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

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

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

_Source: src/hooks/types.ts_

### ArrayNode

**Kind:** `class`

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

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

**Examples:**

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

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

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

### ArrayNodeLike

**Kind:** `interface`

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

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

_Source: src/core/types/index.ts_

### AsyncValidatorFn

**Kind:** `type`

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

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

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

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

**Examples:**

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

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

_Source: src/core/types/index.ts_

### AsyncValidatorOptions

**Kind:** `interface`

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

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

_Source: src/core/types/index.ts_

### BehaviorContext

**Kind:** `type`

Контекст для behavior callback функций
Алиас для FormContext

**Signature:**
```typescript
export type BehaviorContext<TForm> = FormContext<TForm>;
```

**Examples:**

```typescript
watchField(path.country, async (country, ctx) => {
  // Прямой типизированный доступ к полям
  const cities = await fetchCities(country);
  ctx.form.city.updateComponentProps({ options: cities });

  // Безопасная установка значения (без циклов)
  ctx.setFieldValue('city', null);
});
```

_Source: src/core/behavior/types.ts_

### BehaviorContextImpl

**Kind:** `class`

Реализация {@link FormContext} для behaviors. Создаётся фреймворком и передаётся
в callback'и behaviors (`watchField`, `computeFrom`, …) — напрямую инстанцировать
не нужно.

**Signature:**
```typescript
export class BehaviorContextImpl<TForm> implements FormContext<TForm> {
  /**
   * Форма с типизированным Proxy-доступом к полям
   */ /* … */ }
```

**Examples:**

```typescript
import { watchField } from '@reformer/core/behaviors/watch-field';

watchField(path.country, (value, ctx) => {
  // ctx — экземпляр BehaviorContextImpl
  ctx.setFieldValue(path.city, '');
});
```

_Source: src/core/behavior/behavior-context.ts_

### BehaviorHandlerFn

**Kind:** `type`

Функция-handler для behavior

Создает effect подписку для реактивного поведения формы.

**Signature:**
```typescript
export type BehaviorHandlerFn<TForm> = (
  form: GroupNode<TForm>,
  context: BehaviorContext<TForm>,
  withDebounce: (callback: () => void) => void
) => (() => void) | null;
```

**Parameters:**
- `form` — - Корневой узел формы (GroupNode)
- `context` — - Контекст для работы с формой
- `withDebounce` — - Функция-обертка для debounce

**Returns:** Функция cleanup для отписки от effect или null

**Examples:**

```typescript
const handler: BehaviorHandlerFn<MyForm> = (form, context, withDebounce) => {
  const sourceNode = form.getFieldByPath('email');

  return effect(() => {
    const value = sourceNode.value.value;
    withDebounce(() => {
      // Логика behavior
    });
  });
};
```

_Source: src/core/behavior/types.ts_

### BehaviorOptions

**Kind:** `interface`

Общие опции для behavior

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

_Source: src/core/behavior/types.ts_

### BehaviorRegistry

**Kind:** `class`

Реестр behaviors для формы

Каждый экземпляр GroupNode создает собственный реестр (композиция).
Устраняет race conditions и изолирует формы друг от друга.

Наследует AbstractRegistry для унификации:
- Управления global stack
- Template methods begin/end registration

**Signature:**
```typescript
export class BehaviorRegistry extends AbstractRegistry<RegisteredBehavior> {
  /**
   * Получить текущий активный реестр из context stack
   *
   * @returns Текущий активный реестр или null
   *
   * @example
   * ```typescript
   * // В schema-behaviors.ts
   * export function copyFrom(...) {
   *   const registry = BehaviorRegistry.getCurrent();
   *   if (registry) {
   *     registry.register({ ... });
   *   }
   * }
   * ```
   */ /* … */ }
```

**Examples:**

```typescript
class GroupNode {
  private readonly behaviorRegistry = new BehaviorRegistry();

  applyBehaviorSchema(schemaFn) {
    this.behaviorRegistry.beginRegistration(); // Pushes this to stack
    schemaFn(createBehaviorFieldPath(this));   // Uses getCurrent()
    return this.behaviorRegistry.endRegistration(this); // Pops from stack
  }
}
```

_Source: src/core/behavior/behavior-registry.ts_

### BehaviorSchemaFn

**Kind:** `type`

Тип функции behavior схемы
Принимает FieldPath и описывает поведение формы

**Signature:**
```typescript
export type BehaviorSchemaFn<T> = (path: FieldPath<T>) => void;
```

_Source: src/core/behavior/types.ts_

### computeFrom

**Kind:** `function`

Автоматически вычисляет значение поля на основе других полей

**Signature:**
```typescript
export function computeFrom<TForm, TTarget>(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  sources: FieldPathNode<TForm, any>[],
  target: FieldPathNode<TForm, TTarget>,
  computeFn: (values: TForm) => TTarget,
  options?: ComputeFromOptions<TForm>
): void
```

**Parameters:**
- `sources` — - Массив полей-зависимостей
- `target` — - Поле для записи результата
- `computeFn` — - Функция вычисления (принимает объект с именами полей)
- `options` — - Опции (`debounce`, `condition`, `trigger`)

**Examples:**

Многополевой расчёт — total = price × quantity
```typescript
import { computeFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface OrderForm {
price: number;
quantity: number;
total: number;
}

export const orderBehavior: BehaviorSchemaFn<OrderForm> = (path) => {
computeFrom(
[path.price, path.quantity],
path.total,
(values) =>
(typeof values.price === 'number' ? values.price : 0) *
(typeof values.quantity === 'number' ? values.quantity : 0),
);
};
```

Edge case — async-like дорогие вычисления с `debounce` и условием
```typescript
import { computeFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface MortgageForm {
loanType: 'mortgage' | 'consumer';
loanAmount: number;
loanTerm: number;
interestRate: number;
monthlyPayment: number;
}

function annuity(values: MortgageForm): number {
const { loanAmount, loanTerm, interestRate } = values;
if (!loanAmount || !loanTerm || !interestRate) return 0;
const r = interestRate / 100 / 12;
const n = loanTerm;
return Math.round((loanAmount * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1));
}

export const mortgageBehavior: BehaviorSchemaFn<MortgageForm> = (path) => {
computeFrom(
[path.loanAmount, path.loanTerm, path.interestRate],
path.monthlyPayment,
annuity,
{
debounce: 300,                                  // не пересчитываем на каждый keystroke
condition: (form) => form.loanType === 'mortgage', // считаем только для ипотеки
},
);
};
```

**See also:**
- [docs/llms/20-compute-vs-watch.md](../../../../docs/llms/20-compute-vs-watch.md)

_Source: src/core/behavior/behaviors/compute-from.ts_

### ComputeFromOptions

**Kind:** `interface`

Опции для computeFrom

**Signature:**
```typescript
export interface ComputeFromOptions<TForm> {
  /** Когда вычислять */
  trigger?: 'change' | 'blur';

  /** Debounce в мс */
  debounce?: number;

  /** Условие применения */
  condition?: (form: TForm) => boolean;
}
```

_Source: src/core/behavior/types.ts_

### ConditionFn

**Kind:** `type`

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

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

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

### ConfigWithSchema

**Kind:** `interface`

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

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

_Source: src/core/types/index.ts_

### ConfigWithValue

**Kind:** `interface`

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

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

_Source: src/core/types/index.ts_

### ContextualAsyncValidatorFn

**Kind:** `type`

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

**Signature:**
```typescript
export type ContextualAsyncValidatorFn<TForm, TField> = (
  value: TField,
  ctx: FormContext<TForm>
) => Promise<ValidationError | null>;
```

**Examples:**

```typescript
validateAsync(path.email, async (value, ctx) => {
  const exists = await checkEmailExists(value);
  if (exists) return { code: 'exists', message: 'Email already taken' };
  return null;
});
```

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

### ContextualValidatorFn

**Kind:** `type`

Функция валидации поля с контекстом

Новый паттерн: (value, ctx: FormContext) => ValidationError | null

**Signature:**
```typescript
export type ContextualValidatorFn<TForm, TField> = (
  value: TField,
  ctx: FormContext<TForm>
) => ValidationError | null;
```

**Examples:**

```typescript
validate(path.email, (value, ctx) => {
  if (!value) return { code: 'required', message: 'Email required' };
  const confirm = ctx.form.confirmEmail.value.value;
  if (value !== confirm) return { code: 'mismatch', message: 'Must match' };
  return null;
});
```

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

### copyFrom

**Kind:** `function`

Копирует значения из одного поля/группы в другое при выполнении условия

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

**Parameters:**
- `source` — - Откуда копировать
- `target` — - Куда копировать
- `options` — - Опции копирования (`when`, `fields`, `transform`, `debounce`)

**Examples:**

Скаляр → скаляр с условием
```typescript
import { copyFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';

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

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

Копирование группы с подмножеством полей и `transform`
```typescript
import { copyFrom, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface Address { country: string; region: string; city: string; street: string }
interface ProfileForm {
sameAsRegistration: boolean;
registrationAddress: Address;
residenceAddress: Address;
}

export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
copyFrom(path.registrationAddress, path.residenceAddress, {
when: (form) => form.sameAsRegistration === true,
fields: ['country', 'region', 'city'], // street НЕ копируем
transform: (addr) => ({
...addr,
country: addr.country.toUpperCase(), // нормализация при копировании
}),
debounce: 200,
});
};
```

**See also:**
- [docs/llms/23-copy-from.md](../../../../docs/llms/23-copy-from.md)

_Source: src/core/behavior/behaviors/copy-from.ts_

### CopyFromOptions

**Kind:** `interface`

Опции для copyFrom

**Signature:**
```typescript
export interface CopyFromOptions<TSource, TForm = unknown> {
  /** Условие копирования */
  when?: (form: TForm) => boolean;

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

  /** Трансформация значения */
  transform?: (value: TSource) => unknown;

  /** Debounce в мс */
  debounce?: number;
}
```

_Source: src/core/behavior/types.ts_

### createFieldPath

**Kind:** `function`

Создать FieldPath proxy для формы

**Signature:**
```typescript
export function createFieldPath<T>(): FieldPath<T>
```

**Examples:**

```typescript
const path = createFieldPath<MyForm>();
console.log(path.email.__path); // 'email'
console.log(path.personalData.firstName.__path); // 'personalData.firstName'
```

_Source: src/core/utils/field-path.ts_

### createForm

**Kind:** `function`

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

**Signature:**
```typescript
export function createForm<T>(config: GroupNodeConfig<T>): FormProxy<T>;
```

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

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

**Examples:**

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

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

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

### createTransformer

**Kind:** `function`

Хелпер для создания переиспользуемых трансформаций

**Signature:**
```typescript
export function createTransformer<TValue extends FormValue = FormValue>(
  transformer: (value: TValue) => TValue,
  defaultOptions?: TransformValueOptions
)
```

**Parameters:**
- `transformer` — - Идемпотентная функция преобразования значения
- `defaultOptions` — - Опции, применяемые ко всем вызовам созданного трансформера

**Examples:**

Доменно-специфичные трансформеры (банковский счёт, СНИЛС)
```typescript
import { createTransformer, type BehaviorSchemaFn } from '@reformer/core/behaviors';

// Сохраняем только цифры и форматируем СНИЛС: 000-000-000 00
const formatSnils = createTransformer<string>((v) => {
const d = (v ?? '').replace(/\D/g, '').slice(0, 11);
if (d.length < 9) return d;
return `${d.slice(0, 3)}-${d.slice(3, 6)}-${d.slice(6, 9)}${d.length > 9 ? ' ' + d.slice(9) : ''}`;
});

interface ProfileForm { snils: string }

export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
formatSnils(path.snils, { debounce: 100 });
};
```

С `defaultOptions` — единые настройки на серию полей
```typescript
import { createTransformer, type BehaviorSchemaFn } from '@reformer/core/behaviors';

// Все коды должны быть uppercase, но только после правки пользователем
const upperOnUserEdit = createTransformer<string>(
(v) => (v ?? '').toUpperCase(),
{ onUserChangeOnly: true, debounce: 100 },
);

interface PromoForm { promoCode: string; partnerCode: string }

export const promoBehavior: BehaviorSchemaFn<PromoForm> = (path) => {
upperOnUserEdit(path.promoCode);
upperOnUserEdit(path.partnerCode);
};
```

_Source: src/core/behavior/behaviors/transform-value.ts_

### disableWhen

**Kind:** `function`

Условное выключение поля (инверсия enableWhen)

**Signature:**
```typescript
export function disableWhen<TForm>(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  field: FieldPathNode<TForm, any>,
  condition: (form: TForm) => boolean,
  options?: EnableWhenOptions
): void
```

**Parameters:**
- `field` — - Поле для выключения
- `condition` — - Функция условия (true = disable, false = enable)
- `options` — - Опции (`resetOnDisable`, `debounce`)

**Examples:**

Базовый сценарий — readonly после подтверждения
```typescript
import { disableWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface ConfirmForm {
isConfirmed: boolean;
editableField: string;
}

export const confirmBehavior: BehaviorSchemaFn<ConfirmForm> = (path) => {
// Поле блокируется после установки чекбокса подтверждения
disableWhen(path.editableField, (form) => form.isConfirmed === true);
// resetOnDisable НЕ ставим — сохраняем введённый текст
};
```

С `resetOnDisable` для очистки заблокированного поля
```typescript
import { disableWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface PromoForm {
loanType: 'mortgage' | 'consumer';
promoCode: string;
}

export const promoBehavior: BehaviorSchemaFn<PromoForm> = (path) => {
// Промокод недоступен для потребительских кредитов и сбрасывается
disableWhen(path.promoCode, (form) => form.loanType === 'consumer', {
resetOnDisable: true,
});
};
```

_Source: src/core/behavior/behaviors/enable-when.ts_

### email

**Kind:** `function`

Валидатор формата email

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

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

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path) => [
  required(path.email),
  email(path.email),
]

// С кастомным сообщением
email(path.email, { message: 'Введите корректный email адрес' })
```

```typescript
// Ошибка валидации
{
  code: 'email',
  message: 'Неверный формат email',
  params: {}
}
```

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

### enableWhen

**Kind:** `function`

Условное включение поля на основе значений других полей

**Signature:**
```typescript
export function enableWhen<TForm>(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  field: FieldPathNode<TForm, any>,
  condition: (form: TForm) => boolean,
  options?: EnableWhenOptions
): void
```

**Parameters:**
- `field` — - Поле для включения/выключения
- `condition` — - Функция условия (true = enable, false = disable)
- `options` — - Опции (`resetOnDisable`, `debounce`)

**Examples:**

Базовый сценарий с `resetOnDisable: true`
```typescript
import { enableWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

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

export const loanBehavior: BehaviorSchemaFn<LoanForm> = (path) => {
// Поля ипотеки активны только для loanType === 'mortgage'.
// resetOnDisable: true гарантирует чистые initial values при переключении.
enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage', {
resetOnDisable: true,
});
enableWhen(path.initialPayment, (form) => form.loanType === 'mortgage', {
resetOnDisable: true,
});
};
```

Множественные independent условия + cycle prevention
```typescript
import { enableWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface ProfileForm {
sameAsRegistration: boolean;
employmentStatus: 'employed' | 'selfEmployed' | 'unemployed';
residenceAddress: { city: string; street: string };
companyName: string;
companyInn: string;
businessType: string;
}

export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
// Адрес проживания: enabled, когда НЕ совпадает с регистрационным
enableWhen(path.residenceAddress, (form) => form.sameAsRegistration === false, {
resetOnDisable: true,
});

// Поля работодателя: только для employed
enableWhen(path.companyName, (form) => form.employmentStatus === 'employed', {
resetOnDisable: true,
});
enableWhen(path.companyInn, (form) => form.employmentStatus === 'employed', {
resetOnDisable: true,
});

// ИП-поля: только для selfEmployed
enableWhen(path.businessType, (form) => form.employmentStatus === 'selfEmployed', {
resetOnDisable: true,
});

// ВАЖНО: condition не должен читать значение САМОГО поля — иначе цикл.
// condition зависит ТОЛЬКО от независимых триггеров (loanType, employmentStatus, ...).
};
```

**See also:**
- [docs/llms/22-cycle-detection.md](../../../../docs/llms/22-cycle-detection.md)

_Source: src/core/behavior/behaviors/enable-when.ts_

### EnableWhenOptions

**Kind:** `interface`

Опции для enableWhen/disableWhen

**Signature:**
```typescript
export interface EnableWhenOptions {
  /** Сбросить значение при disable */
  resetOnDisable?: boolean;

  /** Debounce в мс */
  debounce?: number;
}
```

_Source: src/core/behavior/types.ts_

### ErrorFilterOptions

**Kind:** `interface`

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

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

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

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

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

_Source: src/core/types/index.ts_

### ErrorStrategy

**Kind:** `enum`

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

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

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

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

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

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

### extractKey

**Kind:** `function`

Извлечь имя последнего сегмента ({@link FieldPathSegment}) пути.

**Signature:**
```typescript
export function extractKey(node: FieldPathNode<unknown, unknown> | unknown): string
```

**Parameters:**
- `node` — - Узел `FieldPathNode` либо строка-путь.

**Returns:** Имя поля без префикса родителя (последний сегмент).

**Examples:**

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

extractKey(path.user.email); // → 'email'
```

_Source: src/core/utils/field-path.ts_

### extractPath

**Kind:** `function`

Извлечь строковый путь из {@link FieldPathNode}.

**Signature:**
```typescript
export function extractPath(node: FieldPathNode<unknown, unknown> | unknown): string
```

**Parameters:**
- `node` — - Узел `FieldPathNode` либо строка-путь.

**Returns:** Путь вида `"a.b.c"`.

**Examples:**

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

const path = (renderPath) => extractPath(renderPath.user.email); // → 'user.email'
```

_Source: src/core/utils/field-path.ts_

### FieldConfig

**Kind:** `interface`

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

**Signature:**
```typescript
export interface FieldConfig<T> {
  value: T | null;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  component: ComponentType<any>;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  componentProps?: any;
  validators?: ValidatorFn<T>[];
  asyncValidators?: AsyncValidatorFn<T>[];
  disabled?: boolean;
  updateOn?: 'change' | 'blur' | 'submit';
  /** Задержка (в мс) перед запуском асинхронной валидации */
  debounce?: number;
}
```

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

### FieldControlState

**Kind:** `interface`

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

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

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

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

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

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

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

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

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

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

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

**Examples:**

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

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

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

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

_Source: src/hooks/types.ts_

### FieldNode

**Kind:** `class`

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

**Signature:**
```typescript
export class FieldNode<T> extends FormNode<T> {
  // ============================================================================
  // Приватные сигналы
  // ============================================================================ /* … */ }
```

**Examples:**

```typescript
const field = new FieldNode({
  value: '',
  component: Input,
  validators: [required, email],
});

field.setValue('test@mail.com');
await field.validate();
console.log(field.valid.value); // true
```

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

### FieldPath

**Kind:** `type`

FieldPath предоставляет типобезопасный доступ к путям полей формы

Рекурсивно обрабатывает вложенные объекты для поддержки вложенных форм.

Использование:
```typescript
const validation = (path: FieldPath<MyForm>) => {
  required(path.email, { message: 'Email обязателен' });

  // Вложенные объекты
  required(path.registrationAddress.city);
  minLength(path.registrationAddress.street, 3);

  applyWhen(
    path.loanType,
    (type) => type === 'mortgage',
    (path) => {
      required(path.propertyValue, { message: 'Укажите стоимость' });
    }
  );
};
```

**Signature:**
```typescript
export type FieldPath<T> = {
  [K in keyof T]: NonNullable<T[K]> extends Array<unknown>
    ? FieldPathNode<T, T[K], K> // Массивы - не рекурсим (обрабатываются отдельно)
    : NonNullable<T[K]> extends Date | File | Blob | AnyFunction
      ? FieldPathNode<T, T[K], K> // Специальные объекты - не рекурсим
      : NonNullable<T[K]> extends object
        ? FieldPathNode<T, T[K], K> & FieldPath<NonNullable<T[K]>> // Обычные объекты - рекурсия!
        : FieldPathNode<T, T[K], K>; // Примитивы
};
```

_Source: src/core/types/field-path.ts_

### FieldPathNavigator

**Kind:** `class`

Навигация по путям к полям формы

Централизует логику парсинга и навигации по путям к полям формы.
Используется в ValidationContext, BehaviorContext, GroupNode для единообразной
обработки путей вида "address.city" или "items[0].name".

Устраняет дублирование логики парсинга путей в 4 местах кодовой базы.

**Signature:**
```typescript
export class FieldPathNavigator {
  /**
   * Парсит путь в массив сегментов
   *
   * Поддерживаемые форматы:
   * - Простые пути: "name", "email"
   * - Вложенные пути: "address.city", "user.profile.avatar"
   * - Массивы: "items[0]", "items[0].name", "tags[1][0]"
   * - Комбинации: "orders[0].items[1].price"
   *
   * @param path Путь к полю (строка с точками и квадратными скобками)
   * @returns Массив сегментов пути
   *
   * @example
   * ```typescript
   * navigator.parsePath('email');
   * // [{ key: 'email' }]
   *
   * navigator.parsePath('address.city');
   * // [{ key: 'address' }, { key: 'city' }]
   *
   * navigator.parsePath('items[0].name');
   * // [{ key: 'items', index: 0 }, { key: 'name' }]
   * ```
   */ /* … */ }
```

**Examples:**

```typescript
const navigator = new FieldPathNavigator();

// Парсинг пути
const segments = navigator.parsePath('items[0].title');
// [{ key: 'items', index: 0 }, { key: 'title' }]

// Получение значения из объекта
const obj = { items: [{ title: 'Item 1' }] };
const value = navigator.getValueByPath(obj, 'items[0].title');
// 'Item 1'

// Получение узла формы
const titleNode = navigator.getNodeByPath(form, 'items[0].title');
titleNode?.setValue('New Title');
```

_Source: src/core/utils/field-path-navigator.ts_

### FieldPathNode

**Kind:** `interface`

Узел в пути поля
Содержит метаинформацию о поле для валидации

**Signature:**
```typescript
export interface FieldPathNode<TForm, TField, TKey = unknown> {
  /** Ключ поля */
  readonly __key: TKey;
  /** Путь к полю (для вложенных объектов) */
  readonly __path: string;
  /** Тип формы */
  readonly __formType?: TForm;
  /** Тип поля */
  readonly __fieldType?: TField;
}
```

_Source: src/core/types/field-path.ts_

### FieldPathSegment

**Kind:** `type`

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

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

_Source: src/core/types/index.ts_

### FieldStatus

**Kind:** `type`

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

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

_Source: src/core/types/index.ts_

### FormArrayProxy

**Kind:** `type`

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

Объединяет методы и свойства ArrayNode с типизированным доступом к элементам массива.

**Signature:**
```typescript
export type FormArrayProxy<T extends FormFields> = ArrayNode<T> & {
  /**
   * Безопасный доступ к элементу массива по индексу
   * Возвращает GroupNode с типизированными полями или undefined
   */
  at(index: number): FormProxy<T> | undefined;

  /**
   * Итерация по элементам массива с типизированными элементами
   */
  forEach(callback: (item: FormProxy<T>, index: number) => void): void;

  /**
   * Маппинг элементов массива с типизированными элементами
   */
  map<R>(callback: (item: FormProxy<T>, index: number) => R): R[];
};
```

**Examples:**

```typescript
interface TodoItem {
  title: string;
  completed: boolean;
}

const todos: FormArrayProxy<TodoItem> = new ArrayNode(schema);

// Доступ к методам ArrayNode
todos.push({ title: 'New todo', completed: false });
todos.removeAt(0);

// Доступ к элементам (через Proxy)
todos.at(0)?.title.setValue('Updated title');

// Итерация
todos.forEach((item, i) => {
  console.log(item.title.value.value);
});
```

_Source: src/core/types/form-proxy.ts_

### FormChangeCallback

**Kind:** `type`

Callback для обработки событий изменения

**Signature:**
```typescript
export type FormChangeCallback = (event: FormChangeEvent) => void;
```

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

### FormChangeEvent

**Kind:** `interface`

Событие изменения в форме

**Signature:**
```typescript
export interface FormChangeEvent {
  /** Тип изменения */
  type: FormChangeType;
  /** Путь к полю */
  path: string;
  /** Timestamp события */
  timestamp: number;
  /** Старое значение */
  oldValue?: unknown;
  /** Новое значение */
  newValue: unknown;
}
```

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

### FormChangeType

**Kind:** `type`

Тип события изменения в форме

**Signature:**
```typescript
export type FormChangeType = 'value' | 'status' | 'errors' | 'touched' | 'dirty';
```

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

### FormContext

**Kind:** `interface`

Единый контекст для работы с формой

Предоставляет:
- `form` - типизированный Proxy-доступ к полям формы
- `setFieldValue` - безопасная установка значения (emitEvent: false)

**Signature:**
```typescript
export interface FormContext<TForm> {
  /**
   * Форма с типизированным Proxy-доступом к полям
   *
   * Позволяет обращаться к полям напрямую через точечную нотацию:
   * - `ctx.form.email` → FieldNode
   * - `ctx.form.address.city` → FieldNode (вложенный)
   * - `ctx.form.items` → ArrayNode
   *
   * @example
   * ```typescript
   * // Получить значение
   * ctx.form.email.value.value
   *
   * // Установить значение (⚠️ может вызвать цикл в behavior!)
   * ctx.form.email.setValue('new@mail.com')
   *
   * // Безопасно установить значение
   * ctx.form.email.setValue('new@mail.com', { emitEvent: false })
   *
   * // Обновить пропсы компонента
   * ctx.form.city.updateComponentProps({ options: cities })
   *
   * // Валидация поля
   * await ctx.form.email.validate()
   *
   * // Работа с массивами
   * ctx.form.items.push({ title: 'New' })
   * ctx.form.items.clear()
   * ```
   */
  readonly form: FormProxy<TForm>;

  /**
   * Безопасно установить значение поля по строковому пути или FieldPath
   *
   * Автоматически использует `emitEvent: false` для предотвращения
   * бесконечных циклов в behavior схемах.
   *
   * @param path - Путь к полю (строка или FieldPath)
   * @param value - Новое значение
   *
   * @example
   * ```typescript
   * // Сброс города при смене страны (строковый путь)
   * watchField(path.country, (country, ctx) => {
   *   ctx.setFieldValue('city', null);
   * });
   *
   * // Использование FieldPath напрямую (более типобезопасно)
   * watchField(path.country, (country, ctx) => {
   *   ctx.setFieldValue(path.city, null);
   * });
   * ```
   */
  setFieldValue(path: string | FieldPathNode<TForm, unknown>, value: unknown): void;

  /**
   * Получить поле формы по строковому пути
   *
   * Используется для динамического доступа к вложенным полям,
   * особенно в модульных behavior схемах, применяемых через apply().
   *
   * @param path - Строковый путь к полю (например "address.city")
   * @returns FormNode или undefined если поле не найдено
   *
   * @example
   * ```typescript
   * // В модульной behavior схеме, применяемой к вложенному полю:
   * // apply([path.registrationAddress, path.residenceAddress], addressBehavior)
   *
   * watchField(path.region, (region, ctx) => {
   *   // path.city.__path = "registrationAddress.city" или "residenceAddress.city"
   *   const cityField = ctx.getFieldByPath(path.city.__path);
   *   cityField?.updateComponentProps({ options: cities });
   * });
   * ```
   */
  getFieldByPath(path: string): FormNode<FormValue> | undefined;
}
```

_Source: src/core/types/form-context.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> = {
  [K in keyof T]: NonNullable<T[K]> extends Array<infer U>
    ? U extends FormFields
      ? FormArrayProxy<U> // Массив объектов → FormArrayProxy
      : FieldNode<T[K]> // Массив примитивов → FieldNode
    : NonNullable<T[K]> extends FormFields
      ? NonNullable<T[K]> extends Date | File | Blob
        ? FieldNode<T[K]> // Специальные объекты → FieldNode
        : FormProxy<NonNullable<T[K]>> // Обычный объект → FormProxy (рекурсивно!)
      : FieldNode<T[K]>; // Примитивные типы → FieldNode
};
```

_Source: src/core/types/form-proxy.ts_

### FormErrorHandler

**Kind:** `class`

Централизованный обработчик ошибок для форм

Обеспечивает:
- Единообразное логирование ошибок в DEV режиме
- Гибкие стратегии обработки (throw/log/convert)
- Типобезопасное извлечение сообщений из Error/string/unknown

**Signature:**
```typescript
export class FormErrorHandler {
  /**
   * Обработать ошибку согласно заданной стратегии
   *
   * @param error Ошибка для обработки (Error | string | unknown)
   * @param context Контекст ошибки для логирования (например, 'AsyncValidator', 'BehaviorRegistry')
   * @param strategy Стратегия обработки (THROW | LOG | CONVERT)
   * @returns ValidationError если strategy = CONVERT, undefined если strategy = LOG, никогда не возвращается если strategy = THROW
   *
   * @example
   * ```typescript
   * // THROW - пробросить ошибку
   * try {
   *   riskyOperation();
   * } catch (error) {
   *   FormErrorHandler.handle(error, 'RiskyOperation', ErrorStrategy.THROW);
   *   // Этот код никогда не выполнится
   * }
   *
   * // LOG - залогировать и продолжить
   * try {
   *   nonCriticalOperation();
   * } catch (error) {
   *   FormErrorHandler.handle(error, 'NonCritical', ErrorStrategy.LOG);
   *   // Продолжаем выполнение
   * }
   *
   * // CONVERT - конвертировать в ValidationError
   * try {
   *   await validator(value);
   * } catch (error) {
   *   const validationError = FormErrorHandler.handle(
   *     error,
   *     'AsyncValidator',
   *     ErrorStrategy.CONVERT
   *   );
   *   return validationError;
   * }
   * ```
   */ /* … */ }
```

**Examples:**

```typescript
// В async validator (конвертировать в ValidationError)
try {
  await validateEmail(value);
} catch (error) {
  return FormErrorHandler.handle(error, 'EmailValidator', ErrorStrategy.CONVERT);
}

// В behavior applicator (пробросить критичную ошибку)
try {
  applyBehavior(schema);
} catch (error) {
  FormErrorHandler.handle(error, 'BehaviorApplicator', ErrorStrategy.THROW);
}

// В validator (залогировать и продолжить)
try {
  validator(value);
} catch (error) {
  FormErrorHandler.handle(error, 'Validator', ErrorStrategy.LOG);
}
```

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

### FormFields

**Kind:** `type`

Тип для конфига с полями (FormSchema generic constraint)
Используется вместо `Record<string, any>` для схем форм

**Signature:**
```typescript
export type FormFields = Record<string, FormValue>;
```

**Deprecated:** (no message)

_Source: src/core/types/index.ts_

### FormNode

**Kind:** `class`

Абстрактный базовый класс для всех узлов формы.

Все узлы (поля, группы, массивы) наследуют от этого класса и реализуют
единый интерфейс для работы с состоянием и валидацией.

Template Method паттерн используется для управления состоянием:
общие signals (`_touched`, `_dirty`, `_status`) живут в базовом классе,
publi-методы (`markAsTouched`, `disable`, …) реализованы здесь, а protected
hooks (`onMarkAsTouched`, `onDisable`, …) переопределяются в наследниках.

**Signature:**
```typescript
export abstract class FormNode<T> {
  // ============================================================================
  // Protected состояние (для Template Method паттерна)
  // ============================================================================

  /**
   * Пользователь взаимодействовал с узлом (touched)
   * Protected: наследники могут читать/изменять через методы
   */ /* … */ }
```

**Examples:**

```typescript
// FormNode не используется напрямую — экземпляры приходят из getReformerForm.
import { getReformerForm, FormNode } from '@reformer/core';

const form = getReformerForm({ email: '' });
form.email instanceof FormNode; // true
form.email.markAsTouched();
```

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

### FormObserver

**Kind:** `class`

FormObserver - мониторинг изменений в форме

Полезен для:
- Отладки сложных форм
- Логирования изменений для аудита
- Синхронизации с внешними системами

**Signature:**
```typescript
export class FormObserver<T extends FormFields> { /* … */ }
```

**Examples:**

```typescript
const observer = new FormObserver(form, {
  enableLogging: true,
  eventTypes: ['value', 'errors']
});

// Подписка на события
const unsubscribe = observer.subscribe((event) => {
  console.log(`${event.type} at ${event.path}:`, event.newValue);
});

// Включить трассировку
const disposeTracing = observer.enableTracing();

// Cleanup
unsubscribe();
disposeTracing();
```

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

### FormObserverOptions

**Kind:** `interface`

Опции для FormObserver

**Signature:**
```typescript
export interface FormObserverOptions {
  /** Включить логирование в консоль (по умолчанию true в DEV) */
  enableLogging?: boolean;
  /** Фильтр типов событий */
  eventTypes?: FormChangeType[];
  /** Фильтр путей полей (regex или массив) */
  pathFilter?: RegExp | string[];
}
```

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

### FormProxy

**Kind:** `type`

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

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

**Signature:**
```typescript
export type FormProxy<T> = GroupNode<T> & FormControlsProxy<T>;
```

**Examples:**

```typescript
interface UserForm {
  email: string;
  profile: {
    name: string;
    age: number;
  };
}

const form = createForm<UserForm>(schema);

// Доступ к методам GroupNode
await form.validate();
const values = form.getValue();
console.log(form.valid.value);

// Прямой доступ к полям (через Proxy)
form.email.setValue('test@mail.com');
form.profile.name.setValue('John');
```

_Source: src/core/types/form-proxy.ts_

### FormSchema

**Kind:** `type`

Автоматически определяет тип схемы на основе TypeScript типа:
- `T[] -> [FormSchema<T>]` (массив с одним элементом)
- `object -> FormSchema<T>` (группа)
- `primitive -> FieldConfig<T>` (поле)

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

**Signature:**
```typescript
export type FormSchema<T> = {
  [K in keyof T]: NonNullable<T[K]> extends string | number | boolean
    ? FieldConfig<T[K]>
    : NonNullable<T[K]> extends Array<infer U>
      ? U extends string | number | boolean
        ? FieldConfig<T[K]>
        : U extends Date | File | Blob | AnyFunction
          ? FieldConfig<T[K]>
          : [FormSchema<U>]
      : NonNullable<T[K]> extends Date | File | Blob | AnyFunction
        ? FieldConfig<T[K]>
        : FormSchema<NonNullable<T[K]>>;
};
```

**Examples:**

```typescript
interface Form {
  name: string;                    // → FieldConfig<string>
  address: {                       // → FormSchema<Address>
    city: string;
    street: string;
  };
  items?: Array<{                  // → [FormSchema<Item>] (опциональный)
    title: string;
    price: number;
  }>;
}

const schema: FormSchema<Form> = {
  name: { value: '', component: Input },
  address: {
    city: { value: '', component: Input },
    street: { value: '', component: Input },
  },
  items: [{
    title: { value: '', component: Input },
    price: { value: 0, component: Input },
  }],
};
```

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

### FormStatusMachine

**Kind:** `class`

FormStatusMachine - управляет состоянием поля формы

Предоставляет:
- Единый источник истины для статуса
- Computed signals для derived состояний (valid, invalid, pending, disabled)
- Валидацию переходов между состояниями

**Signature:**
```typescript
export class FormStatusMachine {
  /** Внутренний сигнал статуса */ /* … */ }
```

**Examples:**

```typescript
const statusMachine = new FormStatusMachine('valid');

// Начало валидации
statusMachine.startValidation();
console.log(statusMachine.pending.value); // true

// Завершение валидации с ошибками
statusMachine.completeValidation(true);
console.log(statusMachine.invalid.value); // true

// Отключение поля
statusMachine.disable();
console.log(statusMachine.disabled.value); // true
```

_Source: src/core/utils/status-machine.ts_

### FormSubmitter

**Kind:** `class`

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

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

**Examples:**

```typescript
const submitter = new FormSubmitter(form);

// Простой submit
const result = await submitter.submit(async (values) => {
  return await api.saveForm(values);
});

// Проверка состояния
if (submitter.submitting.value) {
  console.log('Форма отправляется...');
}
```

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

### FormValue

**Kind:** `type`

Represents any valid form value type
Use this instead of 'any' for form values to maintain type safety

**Signature:**
```typescript
export type FormValue =
  | string
  | number
  | boolean
  | null
  | undefined
  | Date
  | File
  | FormValue[]
  | { [key: string]: FormValue };
```

_Source: src/core/types/index.ts_

### futureDate

**Kind:** `function`

Проверяет, что дата находится в будущем (не в прошлом)

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

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

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование - дата события должна быть в будущем
validationSchema: (path) => [
  futureDate(path.eventDate),
]

// С кастомным сообщением
futureDate(path.appointmentDate, { message: 'Дата записи должна быть в будущем' })
```

```typescript
// Ошибка валидации
{
  code: 'date_past',
  message: 'Дата не может быть в прошлом',
  params: {}
}
```

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

### getCurrentBehaviorRegistry

**Kind:** `function`

Получить текущий активный BehaviorRegistry из context stack

Используется внутри behavior schema функций (copyFrom, enableWhen, computeFrom и т.д.)
для регистрации поведений в активном контексте GroupNode

**Signature:**
```typescript
export function getCurrentBehaviorRegistry(): BehaviorRegistry
```

**Returns:** Текущий BehaviorRegistry или заглушка в production

**Examples:**

```typescript
function copyFrom(target, source, options) {
  const registry = getCurrentBehaviorRegistry();
  const handler = createCopyBehavior(target, source, options);
  registry.register(handler, { debounce: options?.debounce });
}
```

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

### getCurrentValidationRegistry

**Kind:** `function`

Получить текущий активный ValidationRegistry из context stack

Используется внутри validation schema функций (validate, validateAsync, required и т.д.)
для регистрации валидаторов в активном контексте GroupNode

**Signature:**
```typescript
export function getCurrentValidationRegistry(): ValidationRegistry
```

**Returns:** Текущий ValidationRegistry или заглушка в production

**Examples:**

```typescript
function required(fieldPath, options) {
  const registry = getCurrentValidationRegistry();
  registry.registerSync(path, validator, options);
}
```

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

### getNodeType

**Kind:** `function`

Получить тип узла как строку (для отладки)

Полезно для логирования и отладки

**Signature:**
```typescript
export function getNodeType(node: unknown): string
```

**Parameters:**
- `node` — - Узел для проверки

**Returns:** Строковое название типа узла

**Examples:**

```typescript
console.log('Node type:', getNodeType(node)); // "FieldNode" | "GroupNode" | "ArrayNode" | "FormNode" | "Unknown"
```

_Source: src/core/utils/type-guards.ts_

### GroupNode

**Kind:** `class`

GroupNode - узел для группы полей

Поддерживает два API:
1. Старый API (только schema) - обратная совместимость
2. Новый API (config с form, behavior, validation) - автоматическое применение схем

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

**Examples:**

```typescript
// 1. Старый способ (обратная совместимость)
const simpleForm = new GroupNode({
  email: { value: '', component: Input },
  password: { value: '', component: Input },
});

// 2. Новый способ (с behavior и validation схемами)
const fullForm = new GroupNode({
  form: {
    email: { value: '', component: Input },
    password: { value: '', component: Input },
  },
  behavior: (path) => {
    computeFrom(path.email, [path.email], (values) => values[0]?.trim());
  },
  validation: (path) => {
    required(path.email, { message: 'Email обязателен' });
    email(path.email);
    required(path.password);
    minLength(path.password, 8);
  },
});

// Прямой доступ к полям через Proxy
fullForm.email.setValue('test@mail.com');
await fullForm.validate();
console.log(fullForm.valid.value); // true
```

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

### GroupNodeConfig

**Kind:** `interface`

Конфигурация GroupNode с поддержкой схем
Используется для создания форм с автоматическим применением behavior и validation схем

**Signature:**
```typescript
export interface GroupNodeConfig<T> {
  /** Схема структуры формы (поля и их конфигурация) */
  form: FormSchema<T>;

  /** Схема реактивного поведения (copyFrom, enableWhen, computeFrom и т.д.) */
  behavior?: BehaviorSchemaFn<T>;

  /** Схема валидации (required, email, minLength и т.д.) */
  validation?: ValidationSchemaFn<T>;

  /**
   * Опциональный ValidationRegistry для dependency injection
   * Используется для тестирования с mock-реестрами
   * @internal
   */
  _validationRegistry?: unknown;

  /**
   * Опциональный BehaviorRegistry для dependency injection
   * Используется для тестирования с mock-реестрами
   * @internal
   */
  _behaviorRegistry?: unknown;
}
```

_Source: src/core/types/index.ts_

### isArrayNode

**Kind:** `function`

Проверить, является ли значение ArrayNode (массив форм)

ArrayNode представляет массив вложенных форм (обычно GroupNode)
и имеет array-like методы (push, removeAt, at)

**Signature:**
```typescript
export function isArrayNode(value: unknown): value is ArrayNode<FormFields>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является ArrayNode

**Examples:**

```typescript
if (isArrayNode(node)) {
  node.push(); //  OK - добавить элемент
  node.removeAt(0); //  OK - удалить элемент
  const item = node.at(0); //  OK - получить элемент
}
```

_Source: src/core/utils/type-guards.ts_

### isDate

**Kind:** `function`

Проверяет, что значение является валидной датой

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

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

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path) => [
  isDate(path.birthDate),
  isDate(path.eventDate),
]

// С кастомным сообщением
isDate(path.birthDate, { message: 'Введите корректную дату' })
```

```typescript
// Ошибка валидации
{
  code: 'date_invalid',
  message: 'Неверный формат даты',
  params: {}
}
```

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

### isFieldNode

**Kind:** `function`

Проверить, является ли значение FieldNode (примитивное поле)

FieldNode представляет примитивное поле формы (string, number, boolean и т.д.)
и имеет валидаторы, но не имеет вложенных полей или элементов массива

**Signature:**
```typescript
export function isFieldNode(value: unknown): value is FieldNode<FormValue>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является FieldNode

**Examples:**

```typescript
if (isFieldNode(node)) {
  node.validators; //  OK
  node.asyncValidators; //  OK
  node.markAsTouched(); //  OK
}
```

_Source: src/core/utils/type-guards.ts_

### isFormNode

**Kind:** `function`

Проверить, является ли значение любым FormNode

Проверяет базовые свойства, общие для всех типов узлов

**Signature:**
```typescript
export function isFormNode(value: unknown): value is FormNode<FormValue>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является FormNode

**Examples:**

```typescript
if (isFormNode(value)) {
  value.setValue(newValue);
  value.validate();
}
```

_Source: src/core/utils/type-guards.ts_

### isGroupNode

**Kind:** `function`

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

GroupNode представляет объект с вложенными полями формы
и имеет методы для применения validation/behavior схем

**Signature:**
```typescript
export function isGroupNode(value: unknown): value is GroupNode<FormFields>
```

**Parameters:**
- `value` — - Значение для проверки

**Returns:** true если value является GroupNode

**Examples:**

```typescript
if (isGroupNode(node)) {
  node.applyValidationSchema(schema); //  OK
  node.getFieldByPath('user.email'); //  OK
}
```

_Source: src/core/utils/type-guards.ts_

### max

**Kind:** `function`

Валидатор максимального числового значения

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

**Signature:**
```typescript
export function max<TForm, TField extends number | null | undefined = number>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  maxValue: number,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `maxValue` — - Максимально допустимое значение
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path) => [
  max(path.quantity, 100),
  max(path.discount, 50),
]

// С кастомным сообщением
max(path.quantity, 100, { message: 'Максимум 100 единиц' })
```

```typescript
// Ошибка валидации
{
  code: 'max',
  message: 'Максимальное значение: 100',
  params: { max: 100, actual: 150 }
}
```

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

### maxAge

**Kind:** `function`

Проверяет, что возраст (вычисленный из даты рождения) не больше указанного

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

**Signature:**
```typescript
export function maxAge<TForm, TField extends string | Date | null | undefined = string | Date>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  maxAgeValue: number,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации (дата рождения)
- `maxAgeValue` — - Максимально допустимый возраст в годах
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование - максимум 65 лет
validationSchema: (path) => [
  maxAge(path.birthDate, 65),
]

// С кастомным сообщением
maxAge(path.birthDate, 100, { message: 'Проверьте правильность даты рождения' })
```

```typescript
// Ошибка валидации
{
  code: 'date_max_age',
  message: 'Максимальный возраст: 65 лет',
  params: { maxAge: 65, currentAge: 70 }
}
```

_Source: src/core/validation/validators/max-age.ts_

### maxDate

**Kind:** `function`

Проверяет, что дата не позже указанной максимальной

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

**Signature:**
```typescript
export function maxDate<TForm, TField extends string | Date | null | undefined = string | Date>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  maxDateValue: Date,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `maxDateValue` — - Максимально допустимая дата (включительно)
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path) => [
  maxDate(path.birthDate, new Date()), // Не позже сегодня
]

// С кастомным сообщением
maxDate(path.endDate, new Date('2025-12-31'), { message: 'Дата не может быть позже конца года' })
```

```typescript
// Ошибка валидации
{
  code: 'date_max',
  message: 'Дата должна быть не позднее 31.12.2025',
  params: { maxDate: Date }
}
```

_Source: src/core/validation/validators/max-date.ts_

### maxLength

**Kind:** `function`

Валидатор максимальной длины строки

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

**Signature:**
```typescript
export function maxLength<TForm, TField extends string | null | undefined = string>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  maxLen: number,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `maxLen` — - Максимальная допустимая длина строки
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path) => [
  maxLength(path.name, 50),
  maxLength(path.bio, 500),
]

// С кастомным сообщением
maxLength(path.bio, 500, { message: 'Максимум 500 символов' })
```

```typescript
// Ошибка валидации
{
  code: 'maxLength',
  message: 'Максимальная длина: 500 символов',
  params: { maxLength: 500, actualLength: 512 }
}
```

_Source: src/core/validation/validators/max-length.ts_

### min

**Kind:** `function`

Валидатор минимального числового значения

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

**Signature:**
```typescript
export function min<TForm, TField extends number | null | undefined = number>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  minValue: number,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `minValue` — - Минимально допустимое значение
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path) => [
  min(path.age, 18),
  min(path.quantity, 1),
]

// С кастомным сообщением
min(path.age, 18, { message: 'Вам должно быть не менее 18 лет' })
```

```typescript
// Ошибка валидации
{
  code: 'min',
  message: 'Минимальное значение: 18',
  params: { min: 18, actual: 16 }
}
```

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

### minAge

**Kind:** `function`

Проверяет, что возраст (вычисленный из даты рождения) не меньше указанного

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

**Signature:**
```typescript
export function minAge<TForm, TField extends string | Date | null | undefined = string | Date>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  minAgeValue: number,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации (дата рождения)
- `minAgeValue` — - Минимально допустимый возраст в годах
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование - минимум 18 лет
validationSchema: (path) => [
  minAge(path.birthDate, 18),
]

// С кастомным сообщением
minAge(path.birthDate, 21, { message: 'Вам должно быть не менее 21 года' })
```

```typescript
// Ошибка валидации
{
  code: 'date_min_age',
  message: 'Минимальный возраст: 18 лет',
  params: { minAge: 18, currentAge: 16 }
}
```

_Source: src/core/validation/validators/min-age.ts_

### minDate

**Kind:** `function`

Проверяет, что дата не раньше указанной минимальной

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

**Signature:**
```typescript
export function minDate<TForm, TField extends string | Date | null | undefined = string | Date>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  minDateValue: Date,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `minDateValue` — - Минимально допустимая дата (включительно)
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path) => [
  minDate(path.eventDate, new Date('2024-01-01')),
]

// С кастомным сообщением
minDate(path.startDate, new Date(), { message: 'Дата должна быть не раньше сегодня' })
```

```typescript
// Ошибка валидации
{
  code: 'date_min',
  message: 'Дата должна быть не ранее 01.01.2024',
  params: { minDate: Date }
}
```

_Source: src/core/validation/validators/min-date.ts_

### minLength

**Kind:** `function`

Валидатор минимальной длины строки

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

**Signature:**
```typescript
export function minLength<TForm, TField extends string | null | undefined = string>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  minLen: number,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `minLen` — - Минимальная допустимая длина строки
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path) => [
  minLength(path.name, 2),
  minLength(path.password, 8),
]

// С кастомным сообщением
minLength(path.password, 8, { message: 'Пароль должен быть не менее 8 символов' })
```

```typescript
// Ошибка валидации
{
  code: 'minLength',
  message: 'Минимальная длина: 8 символов',
  params: { minLength: 8, actualLength: 3 }
}
```

_Source: src/core/validation/validators/min-length.ts_

### NodeFactory

**Kind:** `class`

Фабрика для создания узлов формы.

Определяет тип конфига и создаёт соответствующий узел (FieldNode, GroupNode, ArrayNode).
Используется внутри `getReformerForm`/`group`/`array` — явно вызывать обычно не нужно.

**Signature:**
```typescript
export class NodeFactory {
  /**
   * Создает узел формы на основе конфигурации
   *
   * ✅ ОБНОВЛЕНО: Теперь поддерживает массивы напрямую
   *
   * Автоматически определяет тип узла:
   * - FieldNode: имеет value и component
   * - ArrayNode: массив [schema, ...items] или { schema, initialItems }
   * - GroupNode: объект без value, component, schema
   *
   * @param config Конфигурация узла
   * @returns Экземпляр FieldNode, GroupNode или ArrayNode
   * @throws Error если конфиг не соответствует ни одному типу
   *
   * @example
   * ```typescript
   * const factory = new NodeFactory();
   *
   * // FieldNode
   * const field = factory.createNode({
   *   value: 'test@mail.com',
   *   component: Input,
   *   validators: [required, email]
   * });
   *
   * // GroupNode
   * const group = factory.createNode({
   *   email: { value: '', component: Input },
   *   password: { value: '', component: Input }
   * });
   *
   * // ArrayNode (объект)
   * const array = factory.createNode({
   *   schema: { title: { value: '', component: Input } },
   *   initialItems: [{ title: 'Item 1' }]
   * });
   *
   * // ArrayNode (массив) - новый формат
   * const array2 = factory.createNode([
   *   { title: { value: '', component: Input } }, // schema
   *   { title: 'Item 1' }, // initial item 1
   *   { title: 'Item 2' }  // initial item 2
   * ]);
   * ```
   */ /* … */ }
```

**Examples:**

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

const node = NodeFactory.create({ value: '' }); // → FieldNode<string>
```

_Source: src/core/factories/node-factory.ts_

### notEmpty

**Kind:** `function`

Проверить что массив содержит хотя бы один элемент

Это удобный алиас для `minLength(field, 1)`, оптимизированный для массивов.

**Signature:**
```typescript
export function notEmpty<TForm, TItem>(
  fieldPath: FieldPathNode<TForm, TItem[] | undefined> | undefined,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Поле-массив для валидации
- `options` — - Опции валидации (message, params и т.д.)

**Examples:**

```typescript
// Простая проверка
notEmpty(path.properties, { message: 'Добавьте хотя бы один объект имущества' });

// С дополнительными параметрами
notEmpty(path.coBorrowers, {
  message: 'Требуется хотя бы один созаемщик',
  params: { minItems: 1 }
});
```

_Source: src/core/validation/validators/array-validators.ts_

### number

**Kind:** `function`

Адаптер для number валидатора
Проверяет, что значение является числом и соответствует заданным ограничениям

**Signature:**
```typescript
export function number<TForm, TField extends number | undefined = number>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  options?: ValidateOptions & {
    /** Минимальное значение (включительно) */
    min?: number;
    /** Максимальное значение (включительно) */
    max?: number;
    /** Требовать целое число */
    integer?: boolean;
    /** Значение должно быть кратно этому числу */
    multipleOf?: number;
    /** Разрешить отрицательные числа (по умолчанию true) */
    allowNegative?: boolean;
    /** Разрешить ноль (по умолчанию true) */
    allowZero?: boolean;
  }
): void
```

**Examples:**

```typescript
number(path.age);
number(path.price, { min: 0, max: 1000000 });
number(path.percentage, { min: 0, max: 100, integer: true });
number(path.rating, { min: 0, max: 5, multipleOf: 0.5 });
```

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

### ObservableForm

**Kind:** `interface`

Интерфейс формы для observer

**Signature:**
```typescript
export interface ObservableForm<T extends FormFields> {
  value: { value: T };
  status: { value: FieldStatus };
  errors: { value: ValidationError[] };
  touched: { value: boolean };
  dirty: { value: boolean };
  getFieldByPath(path: string): ObservableFormNode | undefined;
}
```

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

### ObservableFormNode

**Kind:** `interface`

Интерфейс узла формы для observer

**Signature:**
```typescript
export interface ObservableFormNode {
  value: { value: FormValue };
  status: { value: FieldStatus };
  errors: { value: ValidationError[] };
  touched: { value: boolean };
  dirty: { value: boolean };
}
```

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

### pastDate

**Kind:** `function`

Проверяет, что дата находится в прошлом (не в будущем)

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

**Signature:**
```typescript
export function pastDate<TForm, TField extends string | Date | undefined = string | Date>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование - дата рождения не может быть в будущем
validationSchema: (path) => [
  pastDate(path.birthDate),
]

// С кастомным сообщением
pastDate(path.birthDate, { message: 'Дата рождения не может быть в будущем' })
```

```typescript
// Ошибка валидации
{
  code: 'date_future',
  message: 'Дата не может быть в будущем',
  params: {}
}
```

_Source: src/core/validation/validators/past-date.ts_

### PathSegment

**Kind:** `interface`

Сегмент пути к полю формы

Представляет один сегмент в пути к полю, например:
- `"email" → { key: 'email' }`
- `"items[0]" → { key: 'items', index: 0 }`

**Signature:**
```typescript
export interface PathSegment {
  /**
   * Ключ поля
   */
  key: string;

  /**
   * Индекс в массиве (опционально)
   * Присутствует только для сегментов вида "items[0]"
   */
  index?: number;
}
```

**Examples:**

```typescript
// Путь "items[0].name" разбивается на:
[
  { key: 'items', index: 0 },
  { key: 'name' }
]
```

_Source: src/core/utils/field-path-navigator.ts_

### pattern

**Kind:** `function`

Валидатор паттерна (регулярного выражения)

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

**Signature:**
```typescript
export function pattern<TForm, TField extends string | undefined = string>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  regex: RegExp,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `regex` — - Регулярное выражение для проверки
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Только буквы
pattern(path.name, /^[а-яА-Яa-zA-Z]+$/)

// Только цифры
pattern(path.code, /^\d+$/)

// С кастомным сообщением
pattern(path.username, /^[a-z0-9_]+$/i, {
  message: 'Только латинские буквы, цифры и подчёркивание'
})
```

```typescript
// Ошибка валидации
{
  code: 'pattern',
  message: 'Значение не соответствует требуемому формату',
  params: { pattern: '^[a-z]+$' }
}
```

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

### phone

**Kind:** `function`

Адаптер для phone валидатора
Поддерживает опциональные поля (string | undefined)

**Signature:**
```typescript
export function phone<TForm, TField extends string | undefined = string>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  options?: ValidateOptions & {
    /** Формат телефона */
    format?: PhoneFormat;
  }
): void
```

**Examples:**

```typescript
phone(path.phoneNumber);
phone(path.phoneNumber, { format: 'ru' });
phone(path.phoneNumber, { format: 'international', message: 'Неверный формат телефона' });
```

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

### PhoneFormat

**Kind:** `type`

Формат телефона для валидации

**Signature:**
```typescript
export type PhoneFormat = 'international' | 'ru' | 'us' | 'any';
```

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

### RegistryStack

**Kind:** `class`

Generic Registry Stack - утилита для управления стеком регистрации

Используется ValidationRegistry и BehaviorRegistry для tracking активного контекста.
Устраняет дублирование кода между параллельными системами.

**Signature:**
```typescript
export class RegistryStack<T> { /* … */ }
```

**Examples:**

```typescript
class ValidationRegistry {
  private static registryStack = new RegistryStack<ValidationRegistry>();

  static getCurrent() {
    return ValidationRegistry.registryStack.getCurrent();
  }

  beginRegistration() {
    ValidationRegistry.registryStack.push(this);
  }

  endRegistration() {
    ValidationRegistry.registryStack.verify(this, 'ValidationRegistry');
  }
}
```

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

### required

**Kind:** `function`

Валидатор обязательного поля

Проверяет, что поле имеет непустое значение.
Пустыми считаются: `null`, `undefined`, `''` (пустая строка).
Для boolean полей требуется значение `true`.

**Signature:**
```typescript
export function required<TForm, TField>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  options?: ValidateOptions
): void
```

**Parameters:**
- `fieldPath` — - Путь к полю для валидации
- `options` — - Опции валидации (message, params)

**Examples:**

```typescript
// Базовое использование
validationSchema: (path, { validate }) => [
  required(path.name),
  required(path.email),
]

// С кастомным сообщением
required(path.phone, { message: 'Укажите номер телефона' })

// Для checkbox (требует true)
required(path.agreeToTerms, { message: 'Необходимо принять условия' })
```

```typescript
// Ошибка валидации
{
  code: 'required',
  message: 'Поле обязательно для заполнения',
  params: {}
}
```

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

### resetWhen

**Kind:** `function`

Условный сброс поля при выполнении условия

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

**Parameters:**
- `field` — - Поле для сброса
- `condition` — - Функция условия (true = reset)
- `options` — - Опции (`resetValue`, `onlyIfDirty`, `debounce`)

**Examples:**

Сброс зависимого поля при смене типа платежа
```typescript
import { resetWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

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

export const checkoutBehavior: BehaviorSchemaFn<CheckoutForm> = (path) => {
// Когда выбрано НЕ card — обнуляем номер карты в пустую строку
resetWhen(path.cardNumber, (form) => form.paymentType !== 'card', {
resetValue: '',
});
};
```

`onlyIfDirty` — не трогаем нетронутые initial значения
```typescript
import { resetWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

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

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

**See also:**
- [docs/llms/25-reset-when.md](../../../../docs/llms/25-reset-when.md)

_Source: src/core/behavior/behaviors/reset-when.ts_

### ResetWhenOptions

**Kind:** `interface`

Опции для resetWhen

**Signature:**
```typescript
export interface ResetWhenOptions {
  /** Значение для сброса (по умолчанию null) */
  resetValue?: FormValue;
  /** Сбросить только если поле dirty */
  onlyIfDirty?: boolean;
}
```

_Source: src/core/behavior/behaviors/reset-when.ts_

### ResourceLoadResult

**Kind:** `type`

Тип для результатов загрузки ресурсов

**Signature:**
```typescript
export type ResourceLoadResult = unknown;
```

_Source: src/core/types/index.ts_

### revalidateWhen

**Kind:** `function`

Перевалидирует поле при изменении других полей

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

**Parameters:**
- `target` — - Поле для перевалидации
- `triggers` — - Поля-триггеры (НЕ должно содержать `target`)
- `options` — - Опции (`debounce`)

**Examples:**

Парная перевалидация — confirmPassword при смене password
```typescript
import { revalidateWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';
import { equalTo } from '@reformer/core/validators';
import type { FieldPath } from '@reformer/core';

interface RegistrationForm { password: string; confirmPassword: string }

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

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

Несколько триггеров + `debounce` для async-валидаторов
```typescript
import { revalidateWhen, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface MortgageForm {
propertyValue: number;
loanAmount: number;
initialPayment: number; // правило: initialPayment >= propertyValue * 0.2 - loanAmount
}

export const mortgageBehavior: BehaviorSchemaFn<MortgageForm> = (path) => {
revalidateWhen(
path.initialPayment,
[path.propertyValue, path.loanAmount],
{ debounce: 300 }, // не дёргаем сервер на каждый keystroke
);
};
```

**See also:**
- [docs/llms/27-revalidate-when.md](../../../../docs/llms/27-revalidate-when.md)

_Source: src/core/behavior/behaviors/revalidate-when.ts_

### RevalidateWhenOptions

**Kind:** `interface`

Опции для revalidateWhen

**Signature:**
```typescript
export interface RevalidateWhenOptions {
  /** Debounce в мс */
  debounce?: number;
}
```

_Source: src/core/behavior/types.ts_

### runOutsideEffect

**Kind:** `function`

Выполняет функцию вне контекста effect

Более простой вариант для одиночного вызова

**Signature:**
```typescript
export function runOutsideEffect(fn: () => void | Promise<void>): void
```

**Parameters:**
- `fn` — - Функция для выполнения

**Examples:**

```typescript
effect(() => {
  const value = signal.value;
  runOutsideEffect(() => {
    otherSignal.value = transform(value);
  });
});
```

_Source: src/core/utils/safe-effect.ts_

### safeCallback

**Kind:** `function`

Создает callback, который выполняется вне контекста effect

Использует queueMicrotask для отложенного выполнения,
что позволяет модифицировать сигналы без вызова "Cycle detected"

**Signature:**
```typescript
export function safeCallback<TArgs extends unknown[]>(
  callback: (...args: TArgs) => void | Promise<void>
): (...args: TArgs) => void
```

**Parameters:**
- `callback` — - Функция для выполнения

**Returns:** Обёрнутая функция, безопасная для вызова внутри effect

**Examples:**

```typescript
// Вместо:
effect(() => {
  queueMicrotask(() => {
    callback(value, context);
  });
});

// Используем:
effect(() => {
  safeCallback(callback)(value, context);
});
```

_Source: src/core/utils/safe-effect.ts_

### safeDebouncedCallback

**Kind:** `function`

Создает версию callback с поддержкой debounce, безопасную для effect

Комбинирует debounce логику с выходом из effect контекста

**Signature:**
```typescript
export function safeDebouncedCallback(
  callback: () => void | Promise<void>,
  withDebounce: (fn: () => void) => void
): () => void
```

**Parameters:**
- `callback` — - Функция для выполнения
- `withDebounce` — - Функция debounce обёртки из BehaviorRegistry

**Returns:** Обёрнутая функция

**Examples:**

```typescript
return effect(() => {
  const value = node.value.value;
  safeDebouncedCallback(
    () => callback(value, context),
    withDebounce
  )();
});
```

_Source: src/core/utils/safe-effect.ts_

### SetValueOptions

**Kind:** `interface`

Опции для setValue

**Signature:**
```typescript
export interface SetValueOptions {
  /** Не вызывать событие изменения (не триггерить валидацию) */
  emitEvent?: boolean;
  /** Обновить только этот узел, не распространять на родителей */
  onlySelf?: boolean;
}
```

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

### StatusEvent

**Kind:** `type`

События для State Machine

**Signature:**
```typescript
export type StatusEvent =
  | { type: 'START_VALIDATION' }
  | { type: 'VALIDATION_SUCCESS' }
  | { type: 'VALIDATION_FAILURE' }
  | { type: 'DISABLE' }
  | { type: 'ENABLE'; hasErrors?: boolean }
  | { type: 'SET_ERRORS'; hasErrors: boolean };
```

_Source: src/core/utils/status-machine.ts_

### SubmitOptions

**Kind:** `interface`

Опции для submit

**Signature:**
```typescript
export interface SubmitOptions {
  /** Пропустить валидацию перед submit */
  skipValidation?: boolean;
  /** Пропустить markAsTouched перед submit */
  skipTouch?: boolean;
}
```

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

### SubmitResult

**Kind:** `interface`

Результат submit

**Signature:**
```typescript
export interface SubmitResult<R> {
  /** Успешно ли выполнен submit */
  success: boolean;
  /** Результат от onSubmit callback */
  data: R | null;
  /** Ошибка, если submit не удался */
  error?: Error;
}
```

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

### SubmittableForm

**Kind:** `interface`

Интерфейс формы для FormSubmitter
Минимальный контракт для работы с любой формой

**Signature:**
```typescript
export interface SubmittableForm<T extends FormFields> {
  /** Пометить все поля как touched */
  markAsTouched(): void;
  /** Валидировать форму */
  validate(): Promise<boolean>;
  /** Получить значения формы */
  getValue(): T;
}
```

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

### SubscriptionManager

**Kind:** `class`

Менеджер подписок для FormNode

Централизует управление effect-подписками в узлах формы,
предотвращает утечки памяти и упрощает отладку.

Каждая подписка имеет уникальный ключ, что позволяет:
- Отписываться от конкретной подписки по ключу
- Автоматически заменять существующие подписки
- Отслеживать количество активных подписок (для отладки)

**Signature:**
```typescript
export class SubscriptionManager {
  /**
   * Хранилище подписок
   * Ключ: уникальный идентификатор подписки
   * Значение: функция отписки (dispose)
   */ /* … */ }
```

**Examples:**

```typescript
class FieldNode {
  private subscriptions = new SubscriptionManager();

  watch(callback: Function) {
    const dispose = effect(() => callback(this.value.value));
    return this.subscriptions.add('watch', dispose);
  }

  dispose() {
    this.subscriptions.clear();
  }
}
```

_Source: src/core/utils/subscription-manager.ts_

### syncFields

**Kind:** `function`

Двусторонняя синхронизация двух полей

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

**Parameters:**
- `field1` — - Первое поле
- `field2` — - Второе поле
- `options` — - Опции (`transform` асимметричен — применяется только field1 → field2; `debounce`)

**Examples:**

Базовый mirror двух текстовых полей
```typescript
import { syncFields, type BehaviorSchemaFn } from '@reformer/core/behaviors';

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

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

С `transform` (асимметричный) и `debounce` для защиты от частых перезаписей
```typescript
import { syncFields, type BehaviorSchemaFn } from '@reformer/core/behaviors';

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

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

**See also:**
- [docs/llms/24-sync-fields.md](../../../../docs/llms/24-sync-fields.md)

_Source: src/core/behavior/behaviors/sync-fields.ts_

### SyncFieldsOptions

**Kind:** `interface`

Опции для syncFields

**Signature:**
```typescript
export interface SyncFieldsOptions<T> {
  /** Трансформация значения */
  transform?: (value: T) => T;

  /** Debounce в мс */
  debounce?: number;
}
```

_Source: src/core/behavior/types.ts_

### toBehaviorFieldPath

**Kind:** `function`

Преобразовать FieldPath во вложенный путь для композиции behavior схем

Аналог toFieldPath из validation API.

**Signature:**
```typescript
export function toBehaviorFieldPath<TForm, TField>(
  fieldPath: FieldPathNode<TForm, TField> | undefined
): FieldPath<TField>
```

**Parameters:**
- `fieldPath` — - Поле для преобразования

**Returns:** Вложенный FieldPath

**Examples:**

```typescript
// address-behavior.ts
export const addressBehavior = (path: FieldPath<Address>) => {
  watchField(path.country, async (country, ctx) => {
    const regions = await fetchRegions(country);
    ctx.updateComponentProps(path.region, { options: regions });
  });
};

// user-behavior.ts
export const userBehavior = (path: FieldPath<User>) => {
  // Композиция: применяем addressBehavior к вложенному полю
  addressBehavior(toBehaviorFieldPath(path.address));
};
```

_Source: src/core/behavior/compose-behavior.ts_

### toFieldPath

**Kind:** `function`

Преобразовать FieldPathNode в FieldPath для переиспользования схем

Позволяет композировать validation schemas:

**Signature:**
```typescript
export function toFieldPath<T>(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  node: FieldPathNode<unknown, T, never> | FieldPathNode<any, T, any>
): FieldPath<T>
```

**Examples:**

```typescript
const personalDataValidation = (path: FieldPath<PersonalData>) => {
  required(path.firstName, { message: 'Имя обязательно' });
  required(path.lastName, { message: 'Фамилия обязательна' });
};

const mainValidation = (path: FieldPath<MyForm>) => {
  //  Переиспользуем схему
  personalDataValidation(toFieldPath(path.personalData));
  required(path.email);
};
```

_Source: src/core/utils/field-path.ts_

### transformers

**Kind:** `const`

Готовые трансформеры для частых случаев. Все идемпотентны и безопасны для повторного применения.

**Signature:**
```typescript
export const transformers
```

**Examples:**

Готовые трансформеры в схеме формы
```typescript
import { transformers, type BehaviorSchemaFn } from '@reformer/core/behaviors';

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

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

Композиция готовых трансформеров через createTransformer
```typescript
import { createTransformer, type BehaviorSchemaFn } from '@reformer/core/behaviors';

// trim + lowercase в одном трансформере (применяется как одна операция)
const normalizeEmail = createTransformer<string>(
(v) => (v ?? '').trim().toLowerCase(),
);

interface ContactForm { email: string }

export const contactBehavior: BehaviorSchemaFn<ContactForm> = (path) => {
normalizeEmail(path.email);
};
```

_Source: src/core/behavior/behaviors/transform-value.ts_

### transformValue

**Kind:** `function`

Трансформация значения поля при изменении
Позволяет автоматически форматировать или преобразовывать значения

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

**Parameters:**
- `field` — - Поле для трансформации
- `transformer` — - Функция трансформации (ОБЯЗАТЕЛЬНО идемпотентная: f(f(x)) === f(x))
- `options` — - Опции (`onUserChangeOnly`, `emitEvent`, `debounce`)

**Examples:**

Базовая нормализация — uppercase + trim email
```typescript
import { transformValue, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface RegistrationForm {
promoCode: string;
email: string;
}

export const registrationBehavior: BehaviorSchemaFn<RegistrationForm> = (path) => {
// Идемпотентно: toUpperCase(toUpperCase(x)) === toUpperCase(x) ✓
transformValue(path.promoCode, (value) => (value ?? '').toUpperCase());
transformValue(path.email, (value) => (value ?? '').trim().toLowerCase());
};
```

`onUserChangeOnly` + idempotent guard для не-тривиальных форматов
```typescript
import { transformValue, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface ProfileForm {
inn: string; // ИНН — только цифры
prefixedCode: string; // должен иметь префикс "ID-"
}

export const profileBehavior: BehaviorSchemaFn<ProfileForm> = (path) => {
// Цифры из ИНН: трансформер идемпотентный естественно
transformValue(path.inn, (v) => (v ?? '').replace(/\D/g, ''));

// Префикс — ВАЖНО guard «уже преобразовано», иначе бесконечный цикл
// f("ID-123") должно === "ID-123", а не "ID-ID-123"
transformValue(
path.prefixedCode,
(v) => (v?.startsWith('ID-') ? v : `ID-${v ?? ''}`),
{
onUserChangeOnly: true, // не трогаем значение из patchValue/preload
debounce: 200,
},
);
};
```

**See also:**
- [docs/llms/26-transform-value.md](../../../../docs/llms/26-transform-value.md)

_Source: src/core/behavior/behaviors/transform-value.ts_

### TransformValueOptions

**Kind:** `interface`

Опции для transformValue

**Signature:**
```typescript
export interface TransformValueOptions {
  /** Трансформировать только при изменении пользователем (не программно) */
  onUserChangeOnly?: boolean;
  /** Триггерить событие изменения после трансформации */
  emitEvent?: boolean;
}
```

_Source: src/core/behavior/behaviors/transform-value.ts_

### TreeValidationContextImpl

**Kind:** `class`

Контекст cross-field валидации. Передаётся в `validateTree`/`validateForm`
callback'и — напрямую инстанцировать не нужно.

**Signature:**
```typescript
export class TreeValidationContextImpl<TForm> extends BaseValidationContext<TForm> { /* … */ }
```

**Examples:**

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

validateForm(form, (ctx) => {
  // ctx — экземпляр TreeValidationContextImpl
  if (ctx.form.startDate.value > ctx.form.endDate.value) {
    return [{ code: 'date-range', message: 'Дата начала позже даты окончания' }];
  }
  return [];
});
```

_Source: src/core/validation/validation-context.ts_

### TreeValidatorFn

**Kind:** `type`

Функция cross-field валидации

**Signature:**
```typescript
export type TreeValidatorFn<TForm> = (ctx: FormContext<TForm>) => ValidationError | null;
```

**Examples:**

```typescript
validateTree((ctx) => {
  const password = ctx.form.password.value.value;
  const confirm = ctx.form.confirmPassword.value.value;
  if (password !== confirm) {
    return { code: 'mismatch', message: 'Passwords must match' };
  }
  return null;
});
```

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

### uniqueId

**Kind:** `function`

Генерирует уникальный идентификатор с указанным префиксом.

**Signature:**
```typescript
export function uniqueId(prefix: SubscriptionKeyType): string
```

**Parameters:**
- `prefix` — - Префикс для идентификатора (используйте {@link SubscriptionKey}).

**Returns:** Уникальный идентификатор в формате `${prefix}-${counter}`.

**Examples:**

```typescript
import { uniqueId, SubscriptionKey } from '@reformer/core';

uniqueId(SubscriptionKey.WatchField); // → 'watchField-1'
uniqueId(SubscriptionKey.WatchField); // → 'watchField-2'
```

_Source: src/core/utils/unique-id.ts_

### UnknownCallback

**Kind:** `type`

Тип для коллбэков и обработчиков событий
Используется вместо (...args: any[]) => any

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

_Source: src/core/types/index.ts_

### UnknownFormValue

**Kind:** `type`

Type-safe alternative to 'any' for unknown form values
Requires explicit type checking before use

**Signature:**
```typescript
export type UnknownFormValue = unknown;
```

_Source: src/core/types/index.ts_

### UnknownRecord

**Kind:** `type`

Тип для Record с unknown значениями
Используется вместо инлайнового `Record<string, unknown>`

**Signature:**
```typescript
export type UnknownRecord = Record<string, any>;
```

_Source: src/core/types/index.ts_

### url

**Kind:** `function`

Адаптер для URL валидатора
Поддерживает опциональные поля (string | undefined)

**Signature:**
```typescript
export function url<TForm, TField extends string | undefined = string>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  options?: ValidateOptions & {
    /** Требовать наличие протокола (http:// или https://) */
    requireProtocol?: boolean;
    /** Разрешенные протоколы */
    allowedProtocols?: string[];
  }
): void
```

**Examples:**

```typescript
url(path.website);
url(path.website, { message: 'Введите корректный URL' });
url(path.website, { requireProtocol: true });
```

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

### useArrayLength

**Kind:** `function`

React-хук для подписки только на длину массива.

Оптимизированная версия {@link useFormControl} для ArrayNode, которая
подписывается только на сигнал `length`. Компонент не будет ре-рендериться
при изменении значений вложенных полей.

**Signature:**
```typescript
export function useArrayLength<T extends FormFields>(control: ArrayNode<T>): number
```

**Parameters:**
- `control` — - ArrayNode для подписки

**Returns:** Текущая длина массива

**Examples:**

```tsx
function ArrayRenderer({ arrayNode }) {
  const length = useArrayLength(arrayNode);

  return (
    <div>
      {arrayNode.map((item, index) => (
        <ItemRenderer key={item.id} item={item} />
      ))}
    </div>
  );
}
```

_Source: src/hooks/useArrayLength.ts_

### useFormControl

**Kind:** `function`

React-хук для подписки на состояние формы (FieldNode или ArrayNode).

Обеспечивает реактивную связь между состоянием формы и React-компонентами.
Использует `useSyncExternalStore` для оптимальной интеграции с React 18+
и Concurrent Mode.

#### Основные возможности

- **Автоматическая подписка** на все сигналы контрола
- **Оптимизация ре-рендеров** - компонент обновляется только при реальных изменениях
- **Поддержка SSR** через `useSyncExternalStore`
- **Типобезопасность** - возвращаемый тип зависит от типа контрола

#### Когда использовать

Используйте `useFormControl` когда компоненту нужен доступ к нескольким
свойствам состояния (value, errors, touched и т.д.).

Для подписки только на значение используйте {@link useFormControlValue} -
это предотвратит лишние ре-рендеры при изменении других свойств.

**Signature:**
```typescript
export function useFormControl(
  control: FieldNode<FormValue> | ArrayNode<FormFields> | undefined
): FieldControlState<FormValue> | ArrayControlState<FormFields>
```

**Parameters:**
- `control` — - FieldNode, ArrayNode или undefined

**Returns:** Объект состояния {@link FieldControlState} или {@link ArrayControlState}

**Examples:**

Текстовое поле с валидацией
```tsx
import { useFormControl } from '@reformer/core';
import type { FieldNode } from '@reformer/core';

interface TextFieldProps {
control: FieldNode<string>;
label: string;
}

function TextField({ control, label }: TextFieldProps) {
const {
value,
disabled,
shouldShowError,
errors,
pending
} = useFormControl(control);

return (
<div className="field">
<label>{label}</label>

<div className="input-wrapper">
<input
 type="text"
 value={value}
 disabled={disabled}
 onChange={e => control.setValue(e.target.value)}
 onBlur={() => control.markAsTouched()}
 aria-invalid={shouldShowError}
/>
{pending && <Spinner />}
</div>

{shouldShowError && errors[0] && (
<span className="error" role="alert">
 {errors[0].message}
</span>
)}
</div>
);
}
```

Checkbox с использованием componentProps
```tsx
interface CheckboxProps {
control: FieldNode<boolean>;
}

function Checkbox({ control }: CheckboxProps) {
const { value, disabled, componentProps } = useFormControl(control);

return (
<label className="checkbox">
<input
type="checkbox"
checked={value}
disabled={disabled}
onChange={e => control.setValue(e.target.checked)}
/>
<span>{componentProps.label}</span>
{componentProps.hint && (
<small>{componentProps.hint}</small>
)}
</label>
);
}

// Использование
control.setComponentProps({
label: 'Accept terms and conditions',
hint: 'Required to continue'
});
```

Select с динамическими опциями
```tsx
interface SelectProps {
control: FieldNode<string>;
}

function Select({ control }: SelectProps) {
const { value, disabled, componentProps, shouldShowError, errors } = useFormControl(control);
const options = componentProps.options as Array<{ value: string; label: string }>;

return (
<div>
<select
value={value}
disabled={disabled}
onChange={e => control.setValue(e.target.value)}
onBlur={() => control.markAsTouched()}
>
<option value="">Select...</option>
{options?.map(opt => (
 <option key={opt.value} value={opt.value}>
   {opt.label}
 </option>
))}
</select>
{shouldShowError && <span className="error">{errors[0]?.message}</span>}
</div>
);
}
```

Динамический массив элементов
```tsx
interface Address {
street: string;
city: string;
}

interface AddressListProps {
control: ArrayNode<Address>;
}

function AddressList({ control }: AddressListProps) {
const { length, valid, dirty, errors } = useFormControl(control);

const handleAdd = () => {
control.push({ street: '', city: '' });
};

const handleRemove = (index: number) => {
control.remove(index);
};

return (
<div className="address-list">
<div className="header">
<h3>Addresses ({length})</h3>
{dirty && <span className="badge">Modified</span>}
</div>

{errors.length > 0 && (
<div className="array-errors">
 {errors.map((e, i) => <p key={i}>{e.message}</p>)}
</div>
)}

{control.map((item, index) => (
<AddressItem
 key={item.id}
 control={item}
 onRemove={() => handleRemove(index)}
/>
))}

{length === 0 && (
<p className="empty">No addresses added yet</p>
)}

<button
onClick={handleAdd}
disabled={length >= 5}
>
Add Address
</button>

{!valid && (
<p className="warning">Please fix errors before submitting</p>
)}
</div>
);
}
```

Условный рендеринг с undefined
```tsx
interface FormProps {
optionalField?: ArrayNode<string>;
}

function Form({ optionalField }: FormProps) {
// При undefined возвращается дефолтное состояние
const { length } = useFormControl(optionalField);

if (!optionalField) {
return null;
}

return <div>Items: {length}</div>;
}
```

**See also:**
- {@link useFormControlValue} - для подписки только на значение
- {@link FieldControlState} - тип состояния для FieldNode
- {@link ArrayControlState} - тип состояния для ArrayNode

_Source: src/hooks/useFormControl.ts_

### useFormControlValue

**Kind:** `function`

React-хук для подписки только на значение поля.

Оптимизированная версия {@link useFormControl}, которая подписывается
только на сигнал `value`. Компонент не будет ре-рендериться при изменении
`errors`, `touched`, `valid` и других свойств состояния.

#### Когда использовать

- **Условный рендеринг** на основе значения другого поля
- **Вычисляемые значения** зависящие от значения поля
- **Read-only отображение** значения без интерактивности
- **Оптимизация производительности** когда не нужны другие свойства состояния

#### Когда НЕ использовать

Если компоненту нужны `errors`, `touched`, `disabled` или другие свойства -
используйте {@link useFormControl}. Множественные подписки на один контрол
через разные хуки менее эффективны, чем одна подписка через `useFormControl`.

**Signature:**
```typescript
export function useFormControlValue<T extends FormValue>(control: FieldNode<T>): T
```

**Parameters:**
- `control` — - FieldNode для подписки на значение

**Returns:** Текущее значение поля

**Examples:**

Условный рендеринг секции
```tsx
import { useFormControlValue } from '@reformer/core';

interface FormFields {
hasShipping: FieldNode<boolean>;
shippingAddress: GroupNode<AddressFields>;
}

function ShippingSection({ form }: { form: FormFields }) {
// Подписка только на значение checkbox
const hasShipping = useFormControlValue(form.hasShipping);

if (!hasShipping) {
return null;
}

return (
<div className="shipping-section">
<h3>Shipping Address</h3>
<AddressForm control={form.shippingAddress} />
</div>
);
}
```

Динамические опции на основе другого поля
```tsx
interface FormFields {
country: FieldNode<string>;
city: FieldNode<string>;
}

function CitySelect({ form }: { form: FormFields }) {
const country = useFormControlValue(form.country);
const { value, disabled } = useFormControl(form.city);

// Получаем города для выбранной страны
const cities = useMemo(() => getCitiesForCountry(country), [country]);

// Сбрасываем город при смене страны
useEffect(() => {
form.city.setValue('');
}, [country, form.city]);

return (
<select
value={value}
disabled={disabled || !country}
onChange={e => form.city.setValue(e.target.value)}
>
<option value="">Select city...</option>
{cities.map(city => (
<option key={city.id} value={city.id}>{city.name}</option>
))}
</select>
);
}
```

Отображение суммы в реальном времени
```tsx
interface OrderItem {
quantity: FieldNode<number>;
price: FieldNode<number>;
}

function OrderTotal({ items }: { items: ArrayNode<OrderItem> }) {
// Для каждого элемента получаем только значения
const quantities = items.map(item => useFormControlValue(item.controls.quantity));
const prices = items.map(item => useFormControlValue(item.controls.price));

const total = quantities.reduce((sum, qty, i) => sum + qty * prices[i], 0);

return (
<div className="order-total">
<strong>Total: ${total.toFixed(2)}</strong>
</div>
);
}
```

Preview значения
```tsx
interface MarkdownEditorProps {
control: FieldNode<string>;
}

function MarkdownPreview({ control }: MarkdownEditorProps) {
// Подписка только на значение для preview
const markdown = useFormControlValue(control);

const html = useMemo(() => marked(markdown), [markdown]);

return (
<div
className="markdown-preview"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}

// Основной редактор использует useFormControl для полного состояния
function MarkdownEditor({ control }: MarkdownEditorProps) {
const { value, shouldShowError, errors } = useFormControl(control);

return (
<div className="editor-container">
<textarea
value={value}
onChange={e => control.setValue(e.target.value)}
/>
{shouldShowError && <span className="error">{errors[0]?.message}</span>}

{/* Preview обновляется только при изменении value *}
<MarkdownPreview control={control} />
</div>
);
}
```

Счётчик символов
```tsx
function CharacterCounter({ control, max }: { control: FieldNode<string>; max: number }) {
const value = useFormControlValue(control);
const remaining = max - value.length;

return (
<span className={remaining < 20 ? 'warning' : ''}>
{remaining} characters remaining
</span>
);
}
```

**See also:**
- {@link useFormControl} - для полного состояния поля
- {@link FieldNode} - тип контрола поля

_Source: src/hooks/useFormControlValue.ts_

### useHiddenCondition

**Kind:** `function`

Хук для реактивной оценки функции hidden.

Подписывается на изменения формы через сигналы и возвращает текущее
значение hidden-условия.

**Signature:**
```typescript
export function useHiddenCondition<T>(
  hiddenFn: HiddenFn<T> | undefined,
  form: FormProxy<T>,
  path: FieldPath<T>
): boolean
```

**Parameters:**
- `hiddenFn` — - Функция, определяющая скрытие.
- `form` — - {@link FormProxy} формы.
- `path` — - Текущий {@link FieldPath}.

**Returns:** `true`, если элемент должен быть скрыт.

**Examples:**

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

function MaybeHidden({ form, path, children }) {
  const hidden = useHiddenCondition((f) => !f.subscribeNewsletter.value, form, path);
  return hidden ? null : <>{children}</>;
}
```

_Source: src/hooks/useHiddenCondition.ts_

### validate

**Kind:** `function`

Зарегистрировать кастомный синхронный валидатор для поля
Поддерживает опциональные поля

**Signature:**
```typescript
export function validate<TForm, TField>(
  fieldPath: FieldPathNode<TForm, TField> | undefined,
  validatorFn: ContextualValidatorFn<TForm, TField>,
  options?: ValidateOptions
): void
```

**Examples:**

```typescript
validate(path.birthDate, (ctx: ValidationContext<TForm, TField>) => {
  const birthDate = new Date(ctx.value());
  const age = calculateAge(birthDate);

  if (age < 18) {
    return {
      code: 'tooYoung',
      message: 'Заемщику должно быть не менее 18 лет',
    };
  }

  return null;
});
```

_Source: src/core/validation/core/validate.ts_

### validateAsync

**Kind:** `function`

Зарегистрировать асинхронный валидатор для поля

**Signature:**
```typescript
export function validateAsync<TForm, TField>(
  fieldPath: FieldPathNode<TForm, TField>,
  validatorFn: ContextualAsyncValidatorFn<TForm, TField>,
  options?: ValidateAsyncOptions
): void
```

**Examples:**

```typescript
validateAsync(
  path.inn,
  async (ctx: ValidationContext<TForm, TField>) => {
    const inn = ctx.value();
    if (!inn) return null;

    const response = await fetch('/api/validate-inn', {
      method: 'POST',
      body: JSON.stringify({ inn }),
    });

    const data = await response.json();
    if (!data.valid) {
      return {
        code: 'invalidInn',
        message: 'ИНН не найден в базе данных ФНС',
      };
    }

    return null;
  },
  { debounce: 1000 }
);
```

_Source: src/core/validation/core/validate-async.ts_

### ValidateAsyncOptions

**Kind:** `interface`

Опции для функции validateAsync

**Signature:**
```typescript
export interface ValidateAsyncOptions extends ValidateOptions {
  /** Задержка перед выполнением валидации (в мс) */
  debounce?: number;
}
```

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

### validateForm

**Kind:** `function`

Валидировать форму в соответствии с указанной схемой

Функция создает временный контекст валидации, применяет валидаторы
из схемы и очищает контекст без сохранения в реестр.

**Signature:**
```typescript
export async function validateForm<T extends FormFields>(
  form: GroupNode<T>,
  schema: ValidationSchemaFn<T>
): Promise<boolean>
```

**Parameters:**
- `form` — - GroupNode для валидации
- `schema` — - Схема валидации (ValidationSchemaFn)

**Returns:** Promise<boolean> - `true` если форма валидна, `false` если есть ошибки

**Examples:**

Multi-step форма: валидация текущего шага
```typescript
const goToNextStep = async () => {
const isValid = await validateForm(form, step1LoanValidation);

if (!isValid) {
form.markAsTouched(); // Показать ошибки
return false;
}

setCurrentStep(2);
return true;
};
```

Полная валидация перед submit
```typescript
const handleSubmit = async () => {
const isValid = await validateForm(form, fullValidationSchema);

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

Условная валидация
```typescript
const schema = isBusinessAccount
? businessValidation
: personalValidation;

const isValid = await validateForm(form, schema);
```

_Source: src/core/validation/validate-form.ts_

### validateItems

**Kind:** `function`

Применить validation schema к каждому элементу массива

Регистрирует схему валидации, которая будет автоматически применяться
к каждому элементу ArrayNode (как существующим, так и новым).

**Signature:**
```typescript
export function validateItems<TForm, TItem>(
  fieldPath: FieldPathNode<TForm, TItem[] | undefined> | undefined,
  itemSchemaFn: ValidationSchemaFn<TItem>
): void
```

**Parameters:**
- `fieldPath` — - Поле-массив для валидации элементов
- `itemSchemaFn` — - Validation schema для одного элемента

**Examples:**

```typescript
import { propertyValidation } from './property-validation';

// В additionalValidation
applyWhen(path.hasProperty, (value) => value === true, (path) => {
  // Проверка что массив не пустой
  notEmpty(path.properties, { message: 'Добавьте хотя бы один объект имущества' });

  // Валидация каждого элемента
  validateItems(path.properties, propertyValidation);
});
```

_Source: src/core/validation/validators/array-validators.ts_

### ValidateOptions

**Kind:** `interface`

Опции для функции validate

**Signature:**
```typescript
export interface ValidateOptions {
  /** Сообщение об ошибке */
  message?: string;
  /** Параметры ошибки */
  params?: FormFields;
}
```

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

### validateTree

**Kind:** `function`

Зарегистрировать cross-field валидатор

Используется для валидации, которая зависит от нескольких полей

**Signature:**
```typescript
export function validateTree<TForm>(
  validatorFn: TreeValidatorFn<TForm>,
  options?: ValidateTreeOptions
): void
```

**Examples:**

```typescript
// Явная типизация ctx для избежания implicit any
validateTree(
  (ctx: { form: MyForm }) => {
    if (ctx.form.initialPayment && ctx.form.propertyValue) {
      if (ctx.form.initialPayment > ctx.form.propertyValue) {
        return {
          code: 'initialPaymentTooHigh',
          message: 'Первоначальный взнос не может превышать стоимость',
        };
      }
    }
    return null;
  },
  { targetField: 'initialPayment' }
);
```

_Source: src/core/validation/core/validate-tree.ts_

### ValidateTreeOptions

**Kind:** `interface`

Опции для функции validateTree

**Signature:**
```typescript
export interface ValidateTreeOptions {
  /** Поле, к которому привязать ошибку */
  targetField?: string;
}
```

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

### ValidationContextImpl

**Kind:** `class`

Контекст валидации одного поля. Создаётся фреймворком и передаётся в валидаторы
(`required`, `validate`, …) — напрямую инстанцировать не нужно.

**Signature:**
```typescript
export class ValidationContextImpl<TForm, TField> extends BaseValidationContext<TForm> { /* … */ }
```

**Examples:**

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

validate(path.password, (value, ctx) => {
  // ctx — экземпляр ValidationContextImpl, доступ к ctx.form для cross-field логики
  if (value !== ctx.form.confirmPassword.value) {
    return { code: 'mismatch', message: 'Пароли не совпадают' };
  }
  return null;
});
```

_Source: src/core/validation/validation-context.ts_

### ValidationError

**Kind:** `interface`

Ошибка валидации

**Signature:**
```typescript
export interface ValidationError {
  code: string;
  message: string;
  params?: FormFields;
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
  severity?: 'error' | 'warning';
}
```

_Source: src/core/types/index.ts_

### ValidationRegistry

**Kind:** `class`

Реестр валидаторов для формы

Каждый экземпляр GroupNode создает собственный реестр (композиция).
Устраняет race conditions и изолирует формы друг от друга.

Наследует AbstractRegistry для унификации:
- Управления global stack
- Template methods begin/end registration

**Signature:**
```typescript
export class ValidationRegistry extends AbstractRegistry<ValidatorRegistration> {
  /** Внутренний стек контекстов для управления condition blocks */ /* … */ }
```

**Examples:**

```typescript
class GroupNode {
  private readonly validationRegistry = new ValidationRegistry();

  applyValidationSchema(schemaFn) {
    this.validationRegistry.beginRegistration(); // Pushes this to global stack
    schemaFn(createFieldPath(this));             // Uses getCurrent()
    this.validationRegistry.endRegistration(this); // Pops from global stack
  }
}
```

_Source: src/core/validation/validation-registry.ts_

### ValidationSchemaFn

**Kind:** `type`

Функция validation schema

Принимает FieldPath и определяет все правила валидации для формы

**Signature:**
```typescript
export type ValidationSchemaFn<T> = (path: FieldPath<T>) => void;
```

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

### ValidatorFn

**Kind:** `type`

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

**Signature:**
```typescript
export type ValidatorFn<T = FormValue> = (value: T) => ValidationError | null;
```

_Source: src/core/types/index.ts_

### ValidatorRegistration

**Kind:** `interface`

Регистрация валидатора в системе

**Signature:**
```typescript
export interface ValidatorRegistration {
  fieldPath: string;
  type: 'sync' | 'async' | 'tree';
  validator:
    | ContextualValidatorFn<unknown, unknown>
    | ContextualAsyncValidatorFn<unknown, unknown>
    | TreeValidatorFn<unknown>;
  options?: ValidateOptions | ValidateAsyncOptions | ValidateTreeOptions;
  condition?: {
    fieldPath: string;
    conditionFn: ConditionFn<unknown>;
  };
}
```

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

### watchField

**Kind:** `function`

Выполняет callback при изменении поля

**Signature:**
```typescript
export function watchField<TForm, TField>(
  field: FieldPathNode<TForm, TField>,
  callback: (value: TField, ctx: BehaviorContext<TForm>) => void | Promise<void>,
  options?: WatchFieldOptions
): void
```

**Parameters:**
- `field` — - Поле для отслеживания
- `callback` — - Функция обратного вызова
- `options` — - Опции (`debounce`, `immediate`)

**Examples:**

Async loader with try/catch + guard + debounce
```typescript
import { watchField, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface AddressForm { region: string; city: string }

export const addressBehavior: BehaviorSchemaFn<AddressForm> = (path) => {
watchField(
path.region,
async (region, ctx) => {
if (!region) {
ctx.form.city.updateComponentProps({ options: [] });
return; // guard: пустое значение не триггерит fetch
}
try {
const { data: cities } = await fetchCities(region);
ctx.form.city.updateComponentProps({ options: cities });
} catch (error) {
console.error('[addressBehavior] failed to load cities:', error);
ctx.form.city.updateComponentProps({ options: [] });
}
},
{ immediate: false, debounce: 300 }, // обязательные опции для async
);
};
```

Sync handler с консолидацией нескольких зависимостей в один watcher
```typescript
import { watchField, type BehaviorSchemaFn } from '@reformer/core/behaviors';

interface InsuranceForm {
insuranceType: 'casco' | 'osago' | 'property' | '';
vehicleVin: string;
propertyType: string;
}

export const insuranceBehavior: BehaviorSchemaFn<InsuranceForm> = (path) => {
// ОДИН watchField на trigger-поле — несколько watcher'ов на одно поле = "Cycle detected"
watchField(
path.insuranceType,
(type, ctx) => {
const isVehicle = type === 'casco' || type === 'osago';
const isProperty = type === 'property';

// Guard — ставим только если состояние реально меняется
if (isVehicle) {
if (ctx.form.vehicleVin.disabled.value) ctx.form.vehicleVin.enable();
} else {
if (!ctx.form.vehicleVin.disabled.value) ctx.form.vehicleVin.disable();
if (ctx.form.vehicleVin.getValue() !== '') ctx.form.vehicleVin.setValue('');
}

if (isProperty) {
if (ctx.form.propertyType.disabled.value) ctx.form.propertyType.enable();
} else {
if (!ctx.form.propertyType.disabled.value) ctx.form.propertyType.disable();
if (ctx.form.propertyType.getValue() !== '') ctx.form.propertyType.setValue('');
}
},
{ immediate: false }, // CRITICAL: предотвращает запуск во время инициализации
);
};
```

**See also:**
- [docs/llms/22-cycle-detection.md](../../../../docs/llms/22-cycle-detection.md)

_Source: src/core/behavior/behaviors/watch-field.ts_

### WatchFieldOptions

**Kind:** `interface`

Опции для watchField

**Signature:**
```typescript
export interface WatchFieldOptions {
  /** Debounce в мс */
  debounce?: number;

  /** Вызвать сразу при инициализации */
  immediate?: boolean;
}
```

_Source: src/core/behavior/types.ts_

### WithBehaviorSchema

**Kind:** `interface`

Интерфейс для узлов с методом applyBehaviorSchema

**Signature:**
```typescript
export interface WithBehaviorSchema {
  applyBehaviorSchema(schemaFn: unknown): void;
}
```

_Source: src/core/types/index.ts_

### WithValidationSchema

**Kind:** `interface`

Интерфейс для узлов с методом applyValidationSchema

**Signature:**
```typescript
export interface WithValidationSchema {
  applyValidationSchema(schemaFn: unknown): void;
}
```

_Source: src/core/types/index.ts_
