@reformer/core 9.1.1 → 10.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +39 -28
  2. package/dist/behaviors-node-DUrqAqp1.js +18 -0
  3. package/dist/behaviors.js +1 -1
  4. package/dist/form/create-form.d.ts +13 -17
  5. package/dist/form/index.d.ts +4 -6
  6. package/dist/form/nodes/group-node.d.ts +2 -15
  7. package/dist/form/types/schema-node.d.ts +13 -9
  8. package/dist/form/types/validation-schema.d.ts +7 -29
  9. package/dist/form/validation/index.d.ts +7 -0
  10. package/dist/form/validation/validators/email.d.ts +1 -1
  11. package/dist/form/validation/validators/file-type.d.ts +31 -0
  12. package/dist/form/validation/validators/file-utils.d.ts +51 -0
  13. package/dist/form/validation/validators/future-date.d.ts +1 -1
  14. package/dist/form/validation/validators/index.d.ts +7 -0
  15. package/dist/form/validation/validators/is-date.d.ts +1 -1
  16. package/dist/form/validation/validators/max-file-size.d.ts +28 -0
  17. package/dist/form/validation/validators/max-files.d.ts +26 -0
  18. package/dist/form/validation/validators/max-total-file-size.d.ts +26 -0
  19. package/dist/form/validation/validators/min-file-size.d.ts +28 -0
  20. package/dist/form/validation/validators/min-files.d.ts +26 -0
  21. package/dist/form/validation/validators/past-date.d.ts +1 -1
  22. package/dist/form/validation/validators/pattern.d.ts +1 -1
  23. package/dist/form/validation/validators/phone.d.ts +1 -1
  24. package/dist/form/validation/validators/url.d.ts +1 -1
  25. package/dist/form/validation-schema.d.ts +93 -0
  26. package/dist/index.js +352 -454
  27. package/dist/signal-node-registry-tiwel-TV.js +11 -0
  28. package/dist/state/behaviors-value.d.ts +3 -3
  29. package/dist/validation.d.ts +2 -0
  30. package/dist/validation.js +115 -0
  31. package/dist/validators.js +196 -65
  32. package/llms.txt +1203 -582
  33. package/package.json +5 -1
  34. package/dist/behaviors-node-BMOI2JJF.js +0 -26
  35. package/dist/form/validate-model-core.d.ts +0 -72
  36. package/dist/form/validate-model.d.ts +0 -27
package/llms.txt CHANGED
@@ -48,11 +48,12 @@
48
48
  | What | Where |
49
49
  | ------------------------------------------------------------------------------------------------------ | --------------------------- |
50
50
  | `createModel`, `createForm` | `@reformer/core` |
51
- | `validateModel`, `validateModelSync`, `validateFormModel` | `@reformer/core` |
51
+ | `validateModel`, `defineValidationSchema`, `validate`, `validateAsync`, `validateWhen`, `cross`, `each`, `apply` | `@reformer/core/validation` |
52
+ | `Rule`, `AsyncRule`, `ValidationSchema` (типы) | `@reformer/core/validation` |
52
53
  | `useFormControl`, `useFormControlValue`, `useArrayLength` | `@reformer/core` |
53
54
  | `FormModel`, `FormProxy`, `FieldNode`, `GroupNode`, `ArrayNode`, `ModelArrayNode` | `@reformer/core` |
54
55
  | `ModelSignals`, `ModelArray`, `ModelValue`, `ModelObject`, `PathAwareSignal` | `@reformer/core` |
55
- | `ModelValidator`, `ValidationError`, `FieldConfig`, `FormSchema`, `FieldControlState` | `@reformer/core` |
56
+ | `ValidationError`, `FieldConfig`, `FormSchema`, `FieldControlState` | `@reformer/core` |
56
57
  | `computeFrom`, `copyFrom`, `watchField`, `enableWhen`, `disableWhen` | `@reformer/core` (примитивы) |
57
58
  | `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen` | `@reformer/core` (примитивы) |
58
59
  | `required`, `min`, `max`, `minLength`, `maxLength`, `email`, `pattern`, `url`, `phone` | `@reformer/core/validators` |
@@ -68,9 +69,12 @@
68
69
  > (`defineFormBehavior` + операторы) экспортируется из `@reformer/core/behaviors`, регистрирует cleanup
69
70
  > сам и передаётся в `createForm({ behavior })`. См. `20-compute-vs-watch.md`.
70
71
 
71
- > **Валидаторы — чистые фабрики.** `required()`, `min(50000)`, `email()` возвращают функцию
72
- > `(value) => ValidationError | null` и кладутся в поле схемы как `validators: [required(), min(50000)]`.
73
- > Валидация запускается через `validateFormModel(model, schema)` / `validateModel(model, schema)`.
72
+ > **Валидация — отдельный слой.** Layout-схема `createForm` НЕ несёт валидаторов. Правила живут в
73
+ > `defineValidationSchema<T>(({ model }) => { validate(model.$.x, [required(), min(50000)]); ... })`
74
+ > из `@reformer/core/validation`; фабрики `required()`, `min(50000)`, `email()` возвращают
75
+ > `Rule<T> = (value) => ValidationError | null` и передаются массивом в `validate(sig, [...])`.
76
+ > Запуск — только внешним раннером `await validateModel(model, schema)` (`Promise<boolean>`, ошибки
77
+ > сам роутит в ноды; `form.validate()`/`form.submit()` schema-валидацию НЕ гоняют).
74
78
 
75
79
  ### Type Values
76
80
 
@@ -110,11 +114,13 @@ const { value, errors, disabled } = useFormControl(control.loanType);
110
114
 
111
115
  Архитектура M1: сначала создаётся **модель данных** (`createModel`), затем **форма**
112
116
  (`createForm({ model, schema })`), где схема привязывает поля к сигналам модели
113
- (`model.$.field`). Валидаторы — чистые фабрики из `@reformer/core/validators`, лежат
114
- прямо в поле схемы (`validators: [...]`).
117
+ (`model.$.field`). Layout-схема НЕ несёт валидаторов — валидация живёт в отдельной
118
+ схеме `defineValidationSchema` из `@reformer/core/validation` и запускается внешним
119
+ раннером `validateModel(model, schema)`.
115
120
 
116
121
  ```typescript
117
- import { createModel, createForm, validateFormModel, type FormProxy } from '@reformer/core';
122
+ import { createModel, createForm, type FormProxy } from '@reformer/core';
123
+ import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
118
124
  import { required, email } from '@reformer/core/validators';
119
125
  import { FormField, Input, Button } from '@reformer/ui-kit';
120
126
 
@@ -127,31 +133,37 @@ type ContactForm = {
127
133
  // 2. Model (источник истины значений)
128
134
  const model = createModel<ContactForm>({ name: '', email: '' });
129
135
 
130
- // 3. Schema: привязка поля к сигналу (model.$.field) + component/componentProps + validators
136
+ // 3. Layout-schema: привязка поля к сигналу (model.$.field) + component/componentProps.
137
+ // БЕЗ validators — валидация в отдельной схеме (шаг 4).
131
138
  const schema = {
132
139
  name: {
133
140
  value: model.$.name,
134
141
  component: Input,
135
142
  componentProps: { label: 'Name', placeholder: 'Your name' },
136
- validators: [required({ message: 'Name is required' })],
137
143
  },
138
144
  email: {
139
145
  value: model.$.email,
140
146
  component: Input,
141
147
  componentProps: { label: 'Email', type: 'email' },
142
- validators: [required({ message: 'Email is required' }), email({ message: 'Invalid email' })],
143
148
  },
144
149
  };
145
150
 
146
- // 4. Form — ноды поверх сигналов модели
151
+ // 4. Validation-schema — отдельный слой (@reformer/core/validation)
152
+ const contactValidation = defineValidationSchema<ContactForm>(({ model }) => {
153
+ validate(model.$.name, [required({ message: 'Name is required' })]);
154
+ validate(model.$.email, [required({ message: 'Email is required' }), email({ message: 'Invalid email' })]);
155
+ });
156
+
157
+ // 5. Form — ноды поверх сигналов модели
147
158
  const form = createForm<ContactForm>({ model, schema });
148
159
 
149
- // 5. Use in React component — thin JSX, FormField does ALL heavy lifting
160
+ // 6. Use in React component — thin JSX, FormField does ALL heavy lifting
150
161
  function ContactFormComponent() {
151
162
  const handleSubmit = async (e: React.FormEvent) => {
152
163
  e.preventDefault();
153
- const result = await validateFormModel(model, schema);
154
- if (result.valid) {
164
+ // form.submit()/validate() НЕ гоняют schema-валидацию — только внешний validateModel
165
+ const ok = await validateModel(model, contactValidation); // Promise<boolean>, ошибки сам роутит в ноды
166
+ if (ok) {
155
167
  console.log('Form submitted:', model.get());
156
168
  }
157
169
  };
@@ -165,7 +177,7 @@ function ContactFormComponent() {
165
177
  );
166
178
  }
167
179
 
168
- // 6. Pass form to child components via props (NOT context!)
180
+ // 7. Pass form to child components via props (NOT context!)
169
181
  type FormStepProps = {
170
182
  form: FormProxy<ContactForm>;
171
183
  };
@@ -254,16 +266,19 @@ createForm<T>({ model, schema, behavior? }): FormProxy<T>
254
266
  // form.<field>.enable() / .disable() / .reset() / .markAsTouched() / .setErrors([...])
255
267
  // form.<field>.updateComponentProps({ ... })
256
268
 
257
- // Валидация данных (headless). schema — то же дерево, что в createForm.
258
- validateFormModel<T>(model, schema): Promise<{ valid: boolean; errors: Record<string, ValidationError[]> }>
259
- validateModel<T>(model, schema): Promise<{ valid; errors }> // без роутинга ошибок в ноды
260
- validateModelSync<T>(model, schema): { valid; errors } // async-валидаторы пропускаются
269
+ // Валидация данных — ОТДЕЛЬНЫЙ контракт `@reformer/core/validation` (НЕ дерево createForm: то несёт
270
+ // только layout, без validators). Схема — функция над моделью; внешний раннер разносит ошибки по нодам
271
+ // формы (setErrors), warning не блокирует submit, устаревший прогон отменяется.
272
+ validateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean> // из @reformer/core/validation
273
+ // schema = defineValidationSchema<T>(({ model }) => { validate(...); ... })
274
+ // ⚠️ form.submit() / form.validate() БОЛЬШЕ НЕ прогоняют schema-валидацию — гоняйте validateModel(model, schema) снаружи.
261
275
  ```
262
276
 
263
277
  ### Validators
264
278
 
265
- Валидаторы — **чистые фабрики**: возвращают функцию `(value) => ValidationError | null`.
266
- Кладутся в поле схемы как массив `validators: [required(), min(50000)]`.
279
+ Валидаторы — **чистые фабрики** из `@reformer/core/validators`: возвращают правило `Rule<T>` =
280
+ `(value) => ValidationError | null` (принимают nullable value). Передаются оператору
281
+ `validate(sig, [required(), min(50000)])` внутри схемы валидации, а **не** в layout-дерево `createForm`.
267
282
 
268
283
  ```typescript
269
284
  required(options?: { message?: string })
@@ -291,89 +306,164 @@ minAge(years: number, options?: { message?: string })
291
306
  maxAge(years: number, options?: { message?: string })
292
307
  ```
293
308
 
294
- Использование в схеме:
309
+ Использование: layout-дерево `createForm` привязывает поля к сигналам, правила живут отдельной
310
+ `ValidationSchema` и гоняются раннером `validateModel` по требованию.
295
311
 
296
312
  ```typescript
297
313
  import { createModel, createForm } from '@reformer/core';
314
+ import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
298
315
  import { required, min, max, email } from '@reformer/core/validators';
299
316
 
300
- const model = createModel<{ email: string; age: number; amount: number }>({
301
- email: '', age: 0, amount: 0,
317
+ type Loan = { email: string; age: number; amount: number };
318
+ const model = createModel<Loan>({ email: '', age: 0, amount: 0 });
319
+
320
+ // layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
321
+ const form = createForm({
322
+ model,
323
+ schema: {
324
+ email: { value: model.$.email, component: Input },
325
+ age: { value: model.$.age, component: Input },
326
+ amount: { value: model.$.amount, component: Input },
327
+ },
302
328
  });
303
329
 
304
- const schema = {
305
- email: { value: model.$.email, component: Input, validators: [required(), email()] },
306
- age: { value: model.$.age, component: Input, validators: [required(), min(18)] },
307
- amount: { value: model.$.amount, component: Input, validators: [min(0), max(1000)] },
308
- };
330
+ // правила — в ОТДЕЛЬНОЙ схеме валидации (не в layout)
331
+ const loanValidation = defineValidationSchema<Loan>(({ model }) => {
332
+ validate(model.$.email, [required(), email()]);
333
+ validate(model.$.age, [required(), min(18)]);
334
+ validate(model.$.amount, [min(0), max(1000)]);
335
+ });
309
336
 
310
- const form = createForm({ model, schema });
337
+ const ok = await validateModel(model, loanValidation); // Promise<boolean>; ошибки сами доедут до form.<field>.errors
311
338
  ```
312
339
 
313
340
  ### Custom & cross-field validators
314
341
 
315
- Кастомный валидатор — функция `(value, scope, root) => ValidationError | null | Promise<...>`
316
- (тип `ModelValidator`). `scope` — ближайшая под-модель (элемент массива или корень), `root` —
317
- корневая модель. Cross-field правило читает соседние поля через `root` и вешается на
318
- поле-носитель ошибки:
342
+ Контракт валидации — `@reformer/core/validation`. Схема (`ValidationSchema<T>`) — обычная функция над
343
+ (под)моделью; внутри вызываются свободные операторы, которые сами пишут ошибки в ноды текущего прогона
344
+ (`getNodeForSignal(sig).setErrors(...)` — автор коллектора не видит).
345
+
346
+ ```typescript
347
+ type Rule<T> = (value: T, scope: never, root: never) => ValidationError | null; // scope/root = never (см. ниже)
348
+ type AsyncRule<T> = (value: T, ctx: { signal: AbortSignal }) => Promise<ValidationError | null>;
349
+ type ValidationSchema<T> = (ctx: { model: FormModel<T> }) => void;
350
+ ```
351
+
352
+ Операторы (ambient — валидны только внутри прогона `validateModel`):
353
+
354
+ | Оператор | Назначение |
355
+ |---|---|
356
+ | `validate(sig, rules: Rule<T>[])` | синхронные правила поля |
357
+ | `validateAsync(sig, rules: AsyncRule<T>[])` | асинхронные правила (раннер дожидается, прокидывает `AbortSignal`) |
358
+ | `validateWhen(cond: () => boolean, cb: () => void)` | условная ветка: правила внутри активны/гасятся по `cond` (не трогает enable — это поведение) |
359
+ | `cross(sig, fn: (form) => err \| null)` | cross-field; `fn` получает снапшот модели текущего scope (`model.get()`) |
360
+ | `each(arr, itemFn: (im: FormModel<U>) => void)` | per-item по элементам массива модели |
361
+ | `apply(...schemas: ValidationSchema<T>[])` | композиция под-схем над той же моделью |
362
+ | `defineValidationSchema<T>(fn): ValidationSchema<T>` | тонкая identity-обёртка (типизация/discoverability) |
363
+ | `validateModel<T>(model, schema): Promise<boolean>` | внешний раннер (warning не блокирует, устаревший прогон отменяется) |
364
+
365
+ > `Rule<T>` намеренно несёт `scope: never, root: never` — так value-only фабрики (`required()` = `(value, model, root)`)
366
+ > и inline `(value) => err` ОБА присваиваются в `Rule<T>[]`, сохраняя проверку типа поля
367
+ > (`validate(model.$.age, [email()])` подсветится). Ментальная модель автора — `(value) => error`.
368
+
369
+ Кастомные правила, cross-field и async — в схеме:
319
370
 
320
371
  ```typescript
321
- import type { ModelValidator } from '@reformer/core';
372
+ import { type ValidationError } from '@reformer/core';
373
+ import {
374
+ defineValidationSchema, validate, validateAsync, cross,
375
+ type Rule, type AsyncRule,
376
+ } from '@reformer/core/validation';
377
+ import { required } from '@reformer/core/validators';
322
378
 
323
- // Value-only
324
- const strongPassword: ModelValidator<string> = (value) =>
379
+ type Signup = { password: string; confirm: string; email: string };
380
+
381
+ // Value-only правило (Rule<T>)
382
+ const strongPassword: Rule<string> = (value) =>
325
383
  !value || value.length < 8 ? { code: 'too-short', message: 'Минимум 8 символов' } : null;
326
384
 
327
- // Cross-field: сравниваем с другим полем через root
328
- const passwordsMatch: ModelValidator<string, unknown, { password: string }> = (value, _scope, root) =>
329
- value && root.password && value !== root.password
385
+ // Cross-field: обычная функция над снапшотом модели (model.get()), вешается через cross(sig, fn)
386
+ const passwordsMatch = (f: Signup): ValidationError | null =>
387
+ f.confirm && f.password && f.confirm !== f.password
330
388
  ? { code: 'mismatch', message: 'Пароли не совпадают' }
331
389
  : null;
332
390
 
333
- // Async (проверка уникальности)
334
- const emailUnique: ModelValidator<string> = async (value) => {
391
+ // Async (AsyncRule<T>): получает { signal }; сетевой сбой → null (submit не блокируется)
392
+ const emailUnique: AsyncRule<string> = async (value, { signal }) => {
335
393
  if (!value) return null;
336
- const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`);
337
- const { available } = await res.json();
338
- return available ? null : { code: 'taken', message: 'Email уже зарегистрирован' };
394
+ try {
395
+ const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
396
+ return (await res.json()).available ? null : { code: 'taken', message: 'Email уже зарегистрирован' };
397
+ } catch {
398
+ return null;
399
+ }
339
400
  };
340
401
 
341
- // в схеме
342
- const schema = {
343
- password: { value: model.$.password, component: Input, validators: [strongPassword] },
344
- confirm: { value: model.$.confirm, component: Input, validators: [passwordsMatch] },
345
- email: { value: model.$.email, component: Input, validators: [emailUnique] },
346
- };
402
+ const signupValidation = defineValidationSchema<Signup>(({ model }) => {
403
+ validate(model.$.password, [required(), strongPassword]);
404
+ validate(model.$.confirm, [required()]);
405
+ cross(model.$.confirm, passwordsMatch); // ошибка вешается на confirm
406
+ validateAsync(model.$.email, [emailUnique]);
407
+ });
347
408
  ```
348
409
 
349
- ### Conditional & array validation (schema tree)
410
+ ### Conditional & array validation
350
411
 
351
- `validateFormModel` обходит дерево схемы. Кроме field-узлов (`{ value, validators }`) движок понимает:
412
+ Условные ветки и массивы — операторы `validateWhen` / `each` / `apply` внутри схемы (не узлы дерева):
352
413
 
353
- - **условную ветку** `{ when: (scope, root) => boolean, children: [...] }` — поддерево валидируется
354
- только при истинном `when`; при ложном ошибки полей ветки очищаются;
355
- - **секцию массива** — узел с `componentProps.itemComponent` (`(item) => subSchema`) и `componentProps.control`
356
- (модель-массив с `at`/`length`) — валидируется per-item со scope = под-модель элемента.
414
+ - **условная ветка** `validateWhen(() => cond, () => { validate(...) })` — правила внутри активны только
415
+ при истинном условии; при ложном ошибки полей ветки гасятся (`setErrors([])`). Enable/сброс поля — дело поведения;
416
+ - **массив** `each(model.arr, (im) => { validate(im.$.field, [...]) })` — правила применяются к каждому
417
+ элементу; `im` — под-модель элемента. Для cross-field над элементом захватывайте снапшот в замыкание (`const item = im.get()`);
418
+ - **композиция** `apply(...schemas)` — объединяет под-схемы над той же моделью (напр. полную схему из per-step).
357
419
 
358
420
  ```typescript
359
- // Условная валидация (branch node)
360
- const schema = {
361
- children: [
362
- { value: model.$.loanType, validators: [required()] },
363
- {
364
- when: (form) => form.loanType === 'mortgage',
365
- children: [
366
- { value: model.$.propertyValue, validators: [required(), min(1_000_000)] },
367
- { value: model.$.initialPayment, validators: [required()] },
368
- ],
421
+ import {
422
+ defineValidationSchema, validate, validateWhen, cross, each, apply, validateModel,
423
+ } from '@reformer/core/validation';
424
+ import { required, min } from '@reformer/core/validators';
425
+
426
+ // Условная валидация — ипотечная ветка активна только при loanType === 'mortgage'
427
+ const step1 = defineValidationSchema<LoanForm>(({ model }) => {
428
+ validate(model.$.loanType, [required()]);
429
+ validateWhen(
430
+ () => model.loanType === 'mortgage',
431
+ () => {
432
+ validate(model.$.propertyValue, [required(), min(1_000_000)]);
433
+ validate(model.$.initialPayment, [required()]);
369
434
  },
370
- ],
371
- };
435
+ );
436
+ });
437
+
438
+ // Per-item валидация элементов массива (each над ModelArray)
439
+ const step5 = defineValidationSchema<LoanForm>(({ model }) => {
440
+ each(model.coBorrowers, (im) => {
441
+ const item = im.get(); // снапшот элемента для cross
442
+ validate(im.$.income, [required(), min(10_000)]);
443
+ cross(im.$.income, () =>
444
+ item.income < item.loanShare ? { code: 'tooLow', message: 'Доход ниже доли' } : null);
445
+ });
446
+ });
447
+
448
+ // Полная схема формы = композиция шагов (apply над той же моделью)
449
+ const formValidation = defineValidationSchema<LoanForm>(() => apply(step1, step5));
450
+ ```
451
+
452
+ Wizard-конфиг (per-step + полная валидация) — поверх того же раннера `validateModel`:
453
+
454
+ ```typescript
455
+ const STEP_SCHEMAS = [step1, /* step2, … */ step5] as const;
456
+
457
+ function makeValidationConfig(model: FormModel<LoanForm>) {
458
+ return {
459
+ validateStep: (n: number) => validateModel(model, STEP_SCHEMAS[n - 1]),
460
+ validateAll: () => validateModel(model, formValidation), // formValidation = apply(...STEP_SCHEMAS, extras)
461
+ };
462
+ }
372
463
  ```
373
464
 
374
- > В примерах монорепо эти узлы собираются авторскими хелперами (`field`/`when`/`applyWhen`/`arraySection`)
375
- > — это **локальный typed-сахар в самом примере**, а не экспорт `@reformer/core`. Публичный API — сам
376
- > `validateFormModel(model, schema)` и форма узлов выше.
465
+ Операторы `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply` экспортируются напрямую из
466
+ `@reformer/core/validation` — это публичный API, а не локальный сахар примера.
377
467
 
378
468
  ### Behaviors
379
469
 
@@ -424,10 +514,18 @@ DSL-операторы: `compute` (auto-tracking, без явного списк
424
514
  `apply` (под-схема для группы), `applyEach` (per-item для массива), `exclusiveFlag`, `aggregateInto`.
425
515
  См. `20-compute-vs-watch.md`.
426
516
 
517
+ Поведение **не владеет** валидацией — это отдельный слой. Мост «поведение инициирует валидацию» — через
518
+ `revalidateWhen`, который просто вызывает внешний раннер валидации:
519
+
520
+ ```typescript
521
+ revalidateWhen([model.$.dep], () => void validateModel(model, schema));
522
+ ```
523
+
427
524
  ## 4. 3. COMMON PATTERNS
428
525
 
429
- Все паттерны — на архитектуре M1: значения в модели (`model.$.field`), behaviors на сигналах,
430
- валидация через `validateFormModel`.
526
+ Все паттерны — на архитектуре M1: значения в модели (`model.$.field`), поведение (`defineFormBehavior`)
527
+ на живых сигналах, валидация — отдельной ambient-схемой (`defineValidationSchema`, прогон по требованию
528
+ через `validateModel`). Слои раздельны: layout НЕ несёт validators.
431
529
 
432
530
  ### Conditional Fields with Auto-Reset
433
531
 
@@ -521,64 +619,104 @@ model.get(); // весь объект-снимок
521
619
 
522
620
  ### Submit + validation
523
621
 
622
+ Валидация — отдельная `ValidationSchema<T>` (`defineValidationSchema`), а не часть layout. Раннер
623
+ `validateModel(model, schema)` сам роутит ошибки в ноды формы и возвращает `Promise<boolean>`
624
+ (`false` = есть блокирующая ошибка; `severity: 'warning'` не блокирует).
625
+
524
626
  ```typescript
525
- import { validateFormModel } from '@reformer/core';
627
+ import { validateModel } from '@reformer/core/validation';
526
628
 
527
629
  async function handleSubmit(e: React.FormEvent) {
528
630
  e.preventDefault();
529
- const result = await validateFormModel(model, schema); // ошибки роутятся в ноды формы
530
- if (!result.valid) return; // result.errors — { path: ValidationError[] }
631
+ const ok = await validateModel(model, schema); // ошибки сами доезжают до нод формы (UI подсветит)
632
+ if (!ok) return; // false → есть блокирующая ошибка
531
633
  await api.send(model.get());
532
634
  model.reset(); // к initial-снимку
533
635
  }
534
636
  ```
535
637
 
536
- Multi-step: держи отдельные под-схемы на шаг и вызывай `validateFormModel(model, stepSchema)`.
537
- См. `13-multi-step.md`, `28-submit-and-reset.md`.
638
+ Multi-step: держи отдельные под-схемы на шаг и вызывай `validateModel(model, stepSchema)`. Для wizard'а —
639
+ `makeValidationConfig(model)` → `{ validateStep, validateAll }` (`validateAll` прогоняет полную
640
+ `apply(...STEP_SCHEMAS, extras)`). См. `13-multi-step.md`, `28-submit-and-reset.md`.
538
641
 
539
- ### Cross-field validation — через `root`
642
+ ### Cross-field validation — через `cross`
540
643
 
541
- Cross-field правило — `ModelValidator`, читает соседние поля через `root`, вешается на
542
- поле-носитель ошибки:
644
+ Cross-field правило — обычная функция над **снапшотом** модели (`fn` получает `model.get()`),
645
+ навешивается оператором `cross(sig, fn)` на поле-носитель ошибки внутри схемы:
543
646
 
544
647
  ```typescript
545
- import type { ModelValidator } from '@reformer/core';
648
+ import { defineValidationSchema, validate, cross } from '@reformer/core/validation';
649
+ import { required, min } from '@reformer/core/validators';
650
+ import type { ValidationError } from '@reformer/core';
546
651
 
547
- const initialPaymentVsProperty: ModelValidator<number, unknown, MyForm> = (_value, _scope, root) =>
548
- root.initialPayment && root.propertyValue && root.initialPayment > root.propertyValue
652
+ // снапшот формы читается напрямую — без каста, соседние поля доступны как поля объекта
653
+ const initialPaymentVsProperty = (f: MyForm): ValidationError | null =>
654
+ f.initialPayment && f.propertyValue && f.initialPayment > f.propertyValue
549
655
  ? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }
550
656
  : null;
551
657
 
552
- const schema = {
553
- initialPayment: { value: model.$.initialPayment, component: Input, validators: [initialPaymentVsProperty] },
554
- };
658
+ const schema = defineValidationSchema<MyForm>(({ model }) => {
659
+ validate(model.$.initialPayment, [required(), min(0)]);
660
+ cross(model.$.initialPayment, initialPaymentVsProperty); // ошибка сядет на initialPayment
661
+ });
555
662
  ```
556
663
 
557
- Чтобы правило перезапускалось при изменении зависимости — добавь `revalidateWhen`:
664
+ > Для элемента массива / под-модели захвати нужный снапшот в замыкание (`const item = im.get();
665
+ > cross(im.$.x, () => rule(item))`) — `fn` всегда получает модель ТЕКУЩЕГО scope, а не под-модель.
666
+
667
+ ### Conditional validation — `validateWhen`
668
+
669
+ Условная валидация — `validateWhen(() => cond, () => { … })`: правила внутри активны, только пока
670
+ условие истинно; при `false` ранее тронутые поля гасятся. Это не `enable` (то — поведение), а
671
+ включение/выключение самих проверок:
558
672
 
559
673
  ```typescript
560
- import { revalidateWhen } from '@reformer/core';
561
- revalidateWhen([model.$.propertyValue], () => validateFormModel(model, schema));
674
+ import { validateWhen, validate, cross } from '@reformer/core/validation';
675
+
676
+ validateWhen(
677
+ () => model.loanType === 'mortgage',
678
+ () => {
679
+ validate(model.$.propertyValue, [required(), min(1000000)]);
680
+ cross(model.$.initialPayment, initialPaymentVsProperty);
681
+ }
682
+ );
683
+ ```
684
+
685
+ ### Перезапуск прогона — `revalidateWhen` (мост поведение → валидация)
686
+
687
+ Схема прогоняется по требованию (submit/шаг). Чтобы перезапустить её при изменении зависимости —
688
+ `revalidateWhen` из **поведения** (единственный мост поведение → валидация):
689
+
690
+ ```typescript
691
+ import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
692
+ import { validateModel } from '@reformer/core/validation';
693
+
694
+ const behavior = defineFormBehavior<MyForm>(({ model }) => {
695
+ revalidateWhen([model.$.propertyValue], () => void validateModel(model, schema));
696
+ });
562
697
  ```
563
698
 
564
699
  ### Extracting named rules
565
700
 
566
- Когда тело кастомного валидатора или behavior-оператора растёт — выноси в именованную
567
- функцию, типизированную `ModelValidator<TField, TScope, TRoot>`. Схема остаётся плоской и
568
- читается как оглавление:
701
+ Когда тело правила растёт — выноси в именованную функцию. Field-правило типизируй `Rule<T>`
702
+ (`(value) => ValidationError | null`), cross-field — обычной `(f: Root) => ValidationError | null`.
703
+ Схема остаётся плоской и читается как оглавление:
569
704
 
570
705
  ```typescript
571
- import type { ModelValidator } from '@reformer/core';
706
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
707
+ import { required } from '@reformer/core/validators';
708
+ import type { Rule } from '@reformer/core/validation';
572
709
 
573
- const validateAdultAge: ModelValidator<string> = (value) => {
710
+ // field-правило: (value) => error
711
+ const validateAdultAge: Rule<string> = (value) => {
574
712
  if (!value) return null;
575
713
  const age = new Date().getFullYear() - new Date(value).getFullYear();
576
714
  return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;
577
715
  };
578
716
 
579
- const schema = {
580
- birthDate: { value: model.$.birthDate, component: Input, validators: [validateAdultAge] },
581
- };
717
+ const schema = defineValidationSchema<MyForm>(({ model }) => {
718
+ validate(model.$.birthDate, [required(), validateAdultAge]);
719
+ });
582
720
  ```
583
721
 
584
722
  **Naming convention** (camelCase, семантика, не эхо оператора):
@@ -590,26 +728,41 @@ const schema = {
590
728
 
591
729
  ### Imports rule (#1 cause of cascading errors — read first)
592
730
 
593
- - Модель/форма/валидация/хуки/типы/**примитивы behaviors** — из `@reformer/core`.
594
- - Чистые фабрики валидаторов — из `@reformer/core/validators`.
595
- - Декларативный DSL (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
731
+ - Модель/форма/хуки/типы (`FormModel`, `ValidationError`)/**примитивы behaviors** — из `@reformer/core`.
732
+ - Схема валидации (`defineValidationSchema` + операторы `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`) и раннер `validateModel` — из `@reformer/core/validation`.
733
+ - Чистые фабрики валидаторов (`required`/`min`/`email`/…) — из `@reformer/core/validators`.
734
+ - Декларативный DSL поведения (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
596
735
 
597
736
  ```typescript
598
737
  // ✅ CORRECT
599
738
  import {
600
739
  createModel,
601
740
  createForm,
602
- validateFormModel,
603
741
  useFormControl,
604
742
  useFormControlValue,
605
743
  type FormProxy,
606
744
  type FieldConfig,
607
- type ModelValidator,
745
+ type FormModel,
746
+ type ValidationError,
608
747
  } from '@reformer/core';
748
+ // схема валидации + раннер — отдельный слой:
749
+ import {
750
+ defineValidationSchema,
751
+ validate,
752
+ validateAsync,
753
+ validateWhen,
754
+ cross,
755
+ each,
756
+ apply,
757
+ validateModel,
758
+ type Rule,
759
+ type AsyncRule,
760
+ type ValidationSchema,
761
+ } from '@reformer/core/validation';
609
762
  import { required, min, max, email } from '@reformer/core/validators';
610
763
  // либо примитивы behaviors из основного пакета:
611
764
  import { computeFrom, enableWhen, copyFrom } from '@reformer/core';
612
- // либо декларативный DSL:
765
+ // либо декларативный DSL поведения:
613
766
  import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';
614
767
  ```
615
768
 
@@ -656,38 +809,56 @@ enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true })
656
809
  computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);
657
810
  ```
658
811
 
659
- ### Валидаторы — фабрики в массиве `validators`
812
+ ### Валидаторы — оператор `validate`, а не layout-нода
660
813
 
661
- ```typescript
662
- // ❌ WRONG - нет операторов validate()/applyWhen(); строка вместо options
663
- validate(path.email, required(), 'Email is required');
814
+ Валидация — **отдельный слой** (`@reformer/core/validation`), а не поле layout-ноды. Layout
815
+ (схема `createForm` / JSON-DSL) больше **не несёт** `validators`: правила живут в `ValidationSchema`,
816
+ а прогоняет их внешний раннер `validateModel`. ⚠️ `form.validate()`/`submit()` schema-валидацию
817
+ **НЕ запускают** — прогон только через `validateModel(model, schema)`.
664
818
 
665
- // ✅ CORRECT - фабрика с options, в поле схемы
819
+ ```typescript
820
+ // ❌ WRONG - дерево { value, validators } и позиционная строка удалены
666
821
  const schema = {
667
- email: {
668
- value: model.$.email,
669
- component: Input,
670
- validators: [required({ message: 'Email is required' }), email()],
671
- },
822
+ email: { value: model.$.email, component: Input, validators: [required(), email()] },
672
823
  };
824
+ validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
825
+
826
+ // ✅ CORRECT - операторы validate/validateAsync внутри defineValidationSchema; фабрики с options
827
+ const emailSchema = defineValidationSchema<MyForm>(({ model }) => {
828
+ validate(model.$.email, [required({ message: 'Email is required' }), email()]);
829
+ validateAsync(model.$.email, [
830
+ async (value, { signal }) => {
831
+ const res = await fetch(`/api/free?email=${value}`, { signal });
832
+ return (await res.json()).free ? null : { code: 'taken', message: 'Email занят' };
833
+ },
834
+ ]);
835
+ });
836
+
837
+ // прогон по требованию (submit/шаг): ошибки сами доезжают до нод формы, warnings не блокируют
838
+ const ok = await validateModel(model, emailSchema);
673
839
  ```
674
840
 
675
- ### Cross-field — через `root`, а не `ctx.form`
841
+ ### Cross-field — оператор `cross`, а не `ctx.form`
676
842
 
677
843
  ```typescript
678
- // ❌ WRONG - ctx.form / ctx.setFieldValue / value.value — старый API, не существует
844
+ // ❌ WRONG - ctx.form / ctx.setFieldValue / value.value — старый API; ModelValidator(value,scope,root) удалён
679
845
  watchField(path.amount, (amount, ctx) => {
680
846
  const rate = ctx.form.rate.value.value;
681
847
  ctx.setFieldValue('total', amount * rate);
682
848
  });
683
849
 
684
- // ✅ CORRECT - compute на сигналах; cross-field validator читает root
850
+ // ✅ CORRECT - compute (поведение) на сигналах; cross-field (валидация) через оператор cross
685
851
  compute(model.$.total, () => (model.amount ?? 0) * (model.rate ?? 0));
686
852
 
687
- const amountVsMax: ModelValidator<number, unknown, MyForm> = (value, _s, root) =>
688
- value != null && root.maxAmount != null && value > root.maxAmount
853
+ // cross-field правило — обычная функция над снапшотом Root (`model.get()`), навешивается через cross(sig, fn):
854
+ const amountVsMax = (f: MyForm): ValidationError | null =>
855
+ f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
689
856
  ? { code: 'tooBig', message: 'Превышает лимит' }
690
857
  : null;
858
+
859
+ const amountSchema = defineValidationSchema<MyForm>(({ model }) => {
860
+ cross(model.$.amount, amountVsMax); // fn получает model.get(), вешает ошибку на model.$.amount
861
+ });
691
862
  ```
692
863
 
693
864
  ### Form-shape types — `type` over `interface`
@@ -731,20 +902,16 @@ if (isFieldNode(node)) { ... }
731
902
  | `enableWhen`/`disableWhen` не срабатывает | Поле не материализовано в форме (элемент массива) | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |
732
903
  | `Cycle detected` | Взаимные `compute`/`computeFrom` без стабилизации | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |
733
904
  | Значение поля не пишется | Пишем в форму вместо модели | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` |
734
- | Ошибки не появляются после submit | Не вызвали `validateFormModel` | `await validateFormModel(model, schema)` — он роутит ошибки в ноды |
905
+ | Ошибки не появляются после submit | Не вызвали `validateModel` (`form.submit()` НЕ гоняет schema-валидацию) | `await validateModel(model, schema)` из `@reformer/core/validation` — он роутит ошибки в ноды |
735
906
 
736
907
  ## 7. Import Patterns
737
908
 
738
909
  ```typescript
739
- // Модель, форма, валидация, хуки, типы, примитивы behaviors — из @reformer/core
910
+ // Модель, форма, хуки, типы, примитивы behaviors — из @reformer/core
740
911
  import {
741
912
  // фабрики
742
913
  createModel,
743
914
  createForm,
744
- // валидация данных (headless)
745
- validateModel,
746
- validateModelSync,
747
- validateFormModel,
748
915
  // хуки
749
916
  useFormControl,
750
917
  useFormControlValue,
@@ -771,14 +938,30 @@ import type {
771
938
  ModelArray, // реактивный массив модели (push/removeAt/at/map/length)
772
939
  ModelSignals, // дерево сигналов ($)
773
940
  PathAwareSignal, // сигнал, знающий свой путь
774
- ModelValidator, // (value, scope, root) => ValidationError | null
775
941
  ValidationError,
776
- FieldConfig, // { value, component, componentProps?, validators?, ... }
942
+ FieldConfig, // { value, component, componentProps?, ... } — layout, БЕЗ валидаторов
777
943
  FormSchema,
778
944
  FieldControlState,
779
945
  } from '@reformer/core';
780
946
 
781
- // Валидаторы — чистые фабрики из /validators
947
+ // Схема валидации: операторы + раннер — из /validation
948
+ import {
949
+ defineValidationSchema,
950
+ validate,
951
+ validateAsync,
952
+ validateWhen,
953
+ cross,
954
+ each,
955
+ apply,
956
+ validateModel, // раннер: validateModel(model, schema) => Promise<boolean>
957
+ } from '@reformer/core/validation';
958
+ import type {
959
+ Rule, // (value) => ValidationError | null
960
+ AsyncRule, // (value, { signal }) => Promise<ValidationError | null>
961
+ ValidationSchema,
962
+ } from '@reformer/core/validation';
963
+
964
+ // Валидаторы — чистые фабрики из /validators (кладутся в validate(sig, [...]))
782
965
  import { required, min, max, email, minLength, pattern } from '@reformer/core/validators';
783
966
 
784
967
  // Декларативный DSL behaviors — из /behaviors
@@ -872,7 +1055,9 @@ const model = createModel<MyForm>({
872
1055
  ## 9. 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)
873
1056
 
874
1057
  Под M1 схема **привязывает поле к сигналу модели** (`value: model.$.field`) и держит
875
- UI-конфиг (`component`/`componentProps`) + валидаторы. Значения принадлежат модели.
1058
+ UI-конфиг (`component`/`componentProps`). Значения принадлежат модели. Валидаторов в
1059
+ layout-схеме НЕТ — правила живут в отдельной схеме `defineValidationSchema` из
1060
+ `@reformer/core/validation` и запускаются раннером `validateModel(model, schema)`.
876
1061
 
877
1062
  ### Field node
878
1063
 
@@ -881,8 +1066,6 @@ UI-конфиг (`component`/`componentProps`) + валидаторы. Знач
881
1066
  value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
882
1067
  component: Input, // React-компонент
883
1068
  componentProps?: object, // пропсы (label, placeholder, options, type, ...)
884
- validators?: [...], // чистые фабрики / ModelValidator
885
- asyncValidators?: [...], // async ModelValidator
886
1069
  disabled?: boolean,
887
1070
  updateOn?: 'change' | 'blur' | 'submit',
888
1071
  debounce?: number,
@@ -897,17 +1080,22 @@ UI-конфиг (`component`/`componentProps`) + валидаторы. Знач
897
1080
 
898
1081
  ```typescript
899
1082
  import { createModel, createForm } from '@reformer/core';
1083
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
900
1084
  import { required } from '@reformer/core/validators';
901
1085
  import { Input, Select, Checkbox } from '@reformer/ui-kit';
902
1086
 
903
1087
  const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
904
1088
 
1089
+ // Валидация — отдельным слоем (запуск: await validateModel(model, myValidation))
1090
+ const myValidation = defineValidationSchema<MyForm>(({ model }) => {
1091
+ validate(model.$.name, [required()]);
1092
+ });
1093
+
905
1094
  const schema = {
906
1095
  name: {
907
1096
  value: model.$.name,
908
1097
  component: Input,
909
1098
  componentProps: { label: 'Name', placeholder: 'Enter name' },
910
- validators: [required()],
911
1099
  },
912
1100
  age: {
913
1101
  value: model.$.age,
@@ -1057,13 +1245,16 @@ const form = createForm<MyForm>({ model, schema });
1057
1245
  properties: { array: model.properties, item: propertyItem },
1058
1246
  ```
1059
1247
 
1060
- 2. **Validation-схема `validateFormModel`** — секция массива описывается через `componentProps`:
1061
- `{ componentProps: { control: model.<array>, itemComponent: (item) => subSchema } }`. Движок
1062
- узнаёт секцию по `componentProps.itemComponent` + `control` и обходит её per-item.
1248
+ 2. **Validation-схема (`@reformer/core/validation`)** — per-item правила пишутся оператором
1249
+ `each(arr, (im) => {...})` внутри `defineValidationSchema`; `im` — под-модель элемента
1250
+ (`im.$.field` — его сигналы). Запуск — внешним `validateModel(model, schema)`.
1063
1251
 
1064
1252
  ```typescript
1065
- // узел схемы для validateFormModel(model, schema)
1066
- { componentProps: { control: model.properties, itemComponent: propertyRules } }
1253
+ // внутри defineValidationSchema<MyForm>(({ model }) => { ... })
1254
+ each(model.properties, (im) => {
1255
+ validate(im.$.description, [required()]);
1256
+ validate(im.$.estimatedValue, [required(), min(1)]);
1257
+ });
1067
1258
  ```
1068
1259
 
1069
1260
  3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`ModelArrayNode`),
@@ -1071,9 +1262,9 @@ const form = createForm<MyForm>({ model, schema });
1071
1262
  `@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).
1072
1263
 
1073
1264
  > **Не путай форму по движку.** `createForm` принимает **только** `{ array, item }`;
1074
- > `componentProps.{ control, itemComponent }` — это форма validation-схемы; `FormArray.Root
1075
- > control={form.x}` (или `FormArraySection`) — рендер. `control`/`itemComponent` в layout-схеме
1076
- > `createForm` не подхватятся, а `{ array, item }` схема валидации не обходит.
1265
+ > per-item валидация — это `each(model.<array>, (im) => ...)` в validation-схеме; `FormArray.Root
1266
+ > control={form.x}` (или `FormArraySection`) — рендер. `each` в layout-схеме `createForm`
1267
+ > не подхватится, а `{ array, item }` в validation-схеме не обходится.
1077
1268
 
1078
1269
  ### Array operations — на модели
1079
1270
 
@@ -1141,25 +1332,30 @@ function ItemsList({ form }: { form: FormProxy<MyForm> }) {
1141
1332
 
1142
1333
  ### Array Cross-Validation
1143
1334
 
1144
- Cross-field правило по массиву пишется как `ModelValidator`, читает элементы через `root`,
1145
- вешается на поле-носитель ошибки (или на первый элемент). Секции массива в схеме валидации
1146
- обходятся per-item движком `validateFormModel` (см. `03-api-signatures.md`).
1335
+ Whole-array правило пишется оператором `cross(sig, (f) => ...)`: `f` — снапшот `model.get()`
1336
+ (плоские значения), ошибка вешается на поле-носитель `sig`. Per-item правила — `each`
1337
+ (см. выше, оба — из `@reformer/core/validation`).
1338
+
1339
+ Носитель — любое **скалярное** поле формы (флаг `hasItems`, итоговая сумма и т.п.), у которого
1340
+ есть сигнал `model.$.<field>` и нода в форме:
1147
1341
 
1148
1342
  ```typescript
1149
- import type { ModelValidator } from '@reformer/core';
1343
+ import { cross } from '@reformer/core/validation';
1150
1344
 
1151
- // уникальность имён по массиву
1152
- const uniqueNames: ModelValidator<unknown, unknown, MyForm> = (_value, _scope, root) => {
1153
- const names = root.items.map((i) => i.name);
1345
+ type MyForm = { hasItems: boolean; items: Item[] };
1346
+
1347
+ // внутри defineValidationSchema<MyForm>(({ model }) => { ... })
1348
+ cross(model.$.hasItems, (f: MyForm) => {
1349
+ const names = f.items.map((i) => i.name);
1154
1350
  return names.length !== new Set(names).size
1155
1351
  ? { code: 'duplicate', message: 'Item names must be unique' }
1156
1352
  : null;
1157
- };
1353
+ });
1158
1354
  ```
1159
1355
 
1160
1356
  ## 11. See also
1161
1357
 
1162
- - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item обход `validateFormModel`
1358
+ - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item валидация `each`
1163
1359
  - CDK / ui-kit form-array (`FormArray.Root`, `FormArraySection`) — `find_recipe(topic="form-array")`
1164
1360
 
1165
1361
  ## 12. 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)
@@ -1250,54 +1446,69 @@ clearWhenOff(model.$.hasProperty, model.properties);
1250
1446
 
1251
1447
  ## 14. 12. MULTI-STEP FORM VALIDATION
1252
1448
 
1253
- Каждый шаг — своя под-схема валидации (дерево узлов `{ children: [...] }`). Переход к
1254
- следующему шагу проверяется `validateFormModel(model, stepSchema)`; полный submit — по
1255
- общей схеме (объединение шагов). `validateFormModel` роутит ошибки в ноды формы, поэтому
1256
- UI подсветит проблемные поля текущего шага автоматически.
1449
+ Каждый шаг — своя `ValidationSchema<Form>` (обычная функция `({ model }) => void`, обёрнутая
1450
+ `defineValidationSchema`). Переход к следующему шагу проверяется `validateModel(model, stepSchema)`;
1451
+ полный submit — по общей схеме (композиция шагов через `apply(...)`). `validateModel` сам разносит
1452
+ ошибки по нодам формы (`getNodeForSignal(sig).setErrors(...)`), поэтому UI подсветит проблемные поля
1453
+ текущего шага автоматически. Валидация живёт ОТДЕЛЬНО от layout: сам `RenderNode`/JSON-узел валидаторов
1454
+ не несёт — схема инъектируется в рантайме и роутит ошибки по тем же нодам.
1257
1455
 
1258
1456
  ```typescript
1259
- import { validateFormModel, type FormModel } from '@reformer/core';
1457
+ import { type FormModel } from '@reformer/core';
1458
+ import {
1459
+ validate,
1460
+ apply,
1461
+ defineValidationSchema,
1462
+ validateModel,
1463
+ type ValidationSchema,
1464
+ } from '@reformer/core/validation';
1260
1465
  import { required, min } from '@reformer/core/validators';
1261
1466
 
1262
- // Под-схема шага: дерево field-узлов { value, validators }
1263
- const step1Schema = (m: FormModel<Form>) => ({
1264
- children: [
1265
- { value: m.$.loanType, validators: [required()] },
1266
- { value: m.$.loanAmount, validators: [required(), min(50000)] },
1267
- ],
1467
+ // Под-схема шага — обычная функция ({ model }) => void. Значения проверяет оператор validate(sig, [rules]).
1468
+ const step1Schema = defineValidationSchema<Form>(({ model }) => {
1469
+ validate(model.$.loanType, [required()]);
1470
+ validate(model.$.loanAmount, [required(), min(50000)]);
1268
1471
  });
1269
1472
 
1270
- const step2Schema = (m: FormModel<Form>) => ({
1271
- children: [
1272
- { value: m.$.personalData.firstName, validators: [required()] },
1273
- { value: m.$.personalData.lastName, validators: [required()] },
1274
- ],
1473
+ const step2Schema = defineValidationSchema<Form>(({ model }) => {
1474
+ validate(model.$.personalData.firstName, [required()]);
1475
+ validate(model.$.personalData.lastName, [required()]);
1275
1476
  });
1276
1477
 
1277
- // Карта шагов + полная схема (объединение)
1278
- const STEP_SCHEMAS = [step1Schema, step2Schema];
1279
- const fullSchema = (m: FormModel<Form>) => ({
1280
- children: STEP_SCHEMAS.map((build) => build(m)),
1281
- });
1478
+ // Карта шагов + полная схема (композиция под-схем через apply — заменяет пошаговую группировку деревом)
1479
+ const STEP_SCHEMAS: readonly ValidationSchema<Form>[] = [step1Schema, step2Schema];
1480
+ const fullSchema = defineValidationSchema<Form>(() => apply(...STEP_SCHEMAS));
1282
1481
  ```
1283
1482
 
1284
1483
  ```typescript
1285
- // Переход к следующему шагу
1484
+ // Переход к следующему шагу. validateModel возвращает Promise<boolean> (true = нет блокирующих ошибок;
1485
+ // severity:'warning' не блокирует). Устаревшие прогоны той же (model, schema) отменяются автоматически.
1286
1486
  const goToNextStep = async () => {
1287
- const result = await validateFormModel(model, STEP_SCHEMAS[currentStep - 1](model));
1288
- if (!result.valid) return; // ошибки уже проставлены в ноды текущего шага
1487
+ const ok = await validateModel(model, STEP_SCHEMAS[currentStep - 1]);
1488
+ if (!ok) return; // ошибки уже проставлены в ноды текущего шага
1289
1489
  setCurrentStep(currentStep + 1);
1290
1490
  };
1291
1491
 
1292
- // Полный submit
1492
+ // Полный submit — по общей схеме
1293
1493
  const handleSubmit = async () => {
1294
- const result = await validateFormModel(model, fullSchema(model));
1295
- if (result.valid) {
1494
+ const ok = await validateModel(model, fullSchema);
1495
+ if (ok) {
1296
1496
  await onSubmit(model.get());
1297
1497
  }
1298
1498
  };
1299
1499
  ```
1300
1500
 
1501
+ Слой-потребитель (`FormWizard`) обычно оборачивает это в конфиг с per-step и полной валидацией:
1502
+
1503
+ ```typescript
1504
+ function makeValidationConfig(model: FormModel<Form>) {
1505
+ return {
1506
+ validateStep: (n: number): Promise<boolean> => validateModel(model, STEP_SCHEMAS[n - 1]),
1507
+ validateAll: (): Promise<boolean> => validateModel(model, fullSchema),
1508
+ };
1509
+ }
1510
+ ```
1511
+
1301
1512
  ### Multi-Step Component Example
1302
1513
 
1303
1514
  ```tsx
@@ -1305,8 +1516,8 @@ function MultiStepForm() {
1305
1516
  const [step, setStep] = useState(1);
1306
1517
 
1307
1518
  const nextStep = async () => {
1308
- const result = await validateFormModel(model, STEP_SCHEMAS[step - 1](model));
1309
- if (result.valid) setStep(step + 1);
1519
+ const ok = await validateModel(model, STEP_SCHEMAS[step - 1]);
1520
+ if (ok) setStep(step + 1);
1310
1521
  };
1311
1522
 
1312
1523
  return (
@@ -1385,27 +1596,44 @@ compute(model.$.b, () => model.a + 1); // расходится → Cycle detecte
1385
1596
  compute(model.$.total, () => model.price * model.qty); // одно направление
1386
1597
  ```
1387
1598
 
1388
- ### Cross-field валидация — через `root`
1599
+ ### Cross-field валидация — через `cross(sig, fn)`
1600
+
1601
+ Cross-field живёт в схеме валидации (`@reformer/core/validation`), а не в layout: правило вешается на
1602
+ ПОЛЕ-НОСИТЕЛЬ ошибки оператором `cross(sig, fn)`, а `fn` читает снапшот текущего scope (`model.get()`).
1389
1603
 
1390
1604
  ```typescript
1391
- import type { ModelValidator } from '@reformer/core';
1605
+ import { defineValidationSchema, validate, cross, validateModel } from '@reformer/core/validation';
1606
+ import { required } from '@reformer/core/validators';
1607
+ import { revalidateWhen, type ValidationError } from '@reformer/core';
1392
1608
 
1393
- // Правило вешается на ПОЛЕ-НОСИТЕЛЬ ошибки. Соседние поля читаются через root.
1394
- const field1LessThanField2: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
1609
+ // ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
1610
+ const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
1395
1611
  root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;
1612
+ const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
1613
+ validateFormModel(model, legacySchema);
1396
1614
 
1397
- const schema = {
1398
- field1: { value: model.$.field1, component: Input, validators: [field1LessThanField2] },
1399
- };
1615
+ // ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root
1616
+ const field1LessThanField2 = (f: Form): ValidationError | null =>
1617
+ f.field1 > f.field2 ? { code: 'error', message: 'Invalid' } : null;
1400
1618
 
1401
- // Чтобы правило перезапускалось при изменении field2:
1402
- import { revalidateWhen } from '@reformer/core';
1403
- revalidateWhen([model.$.field2], () => validateFormModel(model, schema));
1619
+ const schema = defineValidationSchema<Form>(({ model }) => {
1620
+ validate(model.$.field1, [required()]);
1621
+ cross(model.$.field1, field1LessThanField2); // правило на поле-носителе ошибки
1622
+ });
1623
+
1624
+ // Прогон — ТОЛЬКО внешним раннером; ошибки сами доезжают до нод формы.
1625
+ // ⚠️ form.validate()/submit() схему валидации больше НЕ прогоняют.
1626
+ await validateModel(model, schema);
1627
+
1628
+ // Перезапуск правила при изменении соседнего поля — мост «поведение → валидация»:
1629
+ revalidateWhen([model.$.field2], () => void validateModel(model, schema));
1404
1630
  ```
1405
1631
 
1406
- > **Удалённый API.** Операторы `validate`/`validateAsync`/`applyWhen`/`apply` (валидации),
1407
- > `validateGroup`/`validateTree`/`validateForm`, типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn`
1408
- > и `ctx.form.*`/`ctx.setFieldValue` — из старой path-based архитектуры и УДАЛЕНЫ. См. `17-nonexistent-api.md`.
1632
+ > **Актуальный vs удалённый API.** Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`
1633
+ > ТЕПЕРЬ существуют — в `@reformer/core/validation` с новыми сигнатурами (ambient, активны только внутри прогона
1634
+ > `validateModel`). УДАЛЕНЫ: `applyWhen`, `validateGroup`/`validateTree`/`validateForm`/`validateFormModel`,
1635
+ > типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn` и `ctx.form.*`/`ctx.setFieldValue` (наследие
1636
+ > path-based архитектуры). См. `17-nonexistent-api.md`.
1409
1637
 
1410
1638
  ## 16. 14. PROJECT STRUCTURE (COLOCATION)
1411
1639
 
@@ -1500,7 +1728,7 @@ forms/
1500
1728
  ├── schema/ # The form definition
1501
1729
  │ ├── model.ts # createModel factory + initial values + array-element factories
1502
1730
  │ ├── schema.ts # createForm schema tree ({ value: model.$.x, component })
1503
- │ ├── validation.ts # validation schema + validateFormModel config
1731
+ │ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
1504
1732
  │ ├── behavior.ts # defineFormBehavior(...)
1505
1733
  │ └── create-form.ts # Assembly: createForm({ model, schema, behavior })
1506
1734
  │
@@ -1692,6 +1920,11 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
1692
1920
 
1693
1921
  ### Integration with UI libraries (shadcn etc.)
1694
1922
 
1923
+ > **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
1924
+ > `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
1925
+ > компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
1926
+ > контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
1927
+
1695
1928
  Если есть существующая design system — оборачивай её компоненты в один
1696
1929
  `MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
1697
1930
  тип input'а — пусть `componentProps.type` диспатчит внутри.
@@ -1720,22 +1953,28 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
1720
1953
 
1721
1954
  ## 18. 15. NON-EXISTENT API (DO NOT USE)
1722
1955
 
1723
- **Следующего API НЕТ в @reformer/core** (частично — наследие старой path-based архитектуры,
1724
- удалено при переходе на M1):
1956
+ **Следующего API НЕТ в @reformer/core** (наследие старой path-based архитектуры,
1957
+ удалено при переходе на M1 и при разделении валидации/поведения):
1958
+
1959
+ > ⚠️ Операторы валидации `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`
1960
+ > **существуют** — но живут в сабпути `@reformer/core/validation` (ambient-схема
1961
+ > `defineValidationSchema(({ model }) => …)`, раннер `validateModel`), а НЕ в корне и НЕ
1962
+ > в `@reformer/core/validators`. Ниже перечислено то, чего действительно нет.
1725
1963
 
1726
1964
  | Wrong | Correct | Notes |
1727
1965
  | -------------------------------- | ------------------------------------------------ | ---------------------------------------------- |
1728
1966
  | `useForm` | `createModel` + `createForm` | Хука useForm нет |
1729
- | `validate`, `validateAsync` | `validators: [...]` в схеме + `validateFormModel`| Операторов валидации нет; валидаторы — фабрики |
1730
- | `applyWhen`, `apply` (валидация) | branch-узел `{ when, children }` в схеме | Условная валидация — узлом дерева схемы |
1731
- | `validateItems`, `validateGroup`, `validateTree` | секция массива в схеме + `ModelValidator` | Удалены |
1732
- | `validateForm` | `validateFormModel(model, schema)` | Legacy-движок удалён |
1733
- | `ValidationSchemaFn`, `BehaviorSchemaFn` | `defineFormBehavior` / `ModelValidator` | Типы path-схем удалены |
1967
+ | `validateForm`, `validateFormModel` | `validateModel(model, schema)` из `@reformer/core/validation` | Legacy-движок дерева `{ value, validators }` удалён; внешний раннер — `validateModel` |
1968
+ | `validateModelSync` | `await validateModel(model, schema)` | Синхронного раннера нет — прогон асинхронный (`Promise<boolean>`) |
1969
+ | `applyWhen` | `validateWhen(() => cond, () => { … })` | Условная валидация — оператором `validateWhen`, не узлом `{ when, children }` |
1970
+ | `validateItems`, `validateGroup`, `validateTree` | `each(model.arr, (im) => { … })` | Per-item — оператором `each`; под-схема — прямой вызов `sub({ model: model.child })` |
1971
+ | `ValidationSchemaFn`, `BehaviorSchemaFn` | `ValidationSchema<T>` (`defineValidationSchema`) / `defineFormBehavior` | Типы path-схем удалены |
1972
+ | `equalTo`, `custom`, `notEmpty` (validators) | inline `Rule<T>` `(value) => err \| null` в `validate(sig, [...])`; сравнение полей — `cross(sig, fn)` | Таких фабрик нет; кастомное правило — обычная функция значения, cross-field — `cross` |
1973
+ | `form.submit()` / `form.validate()` прогоняют схему | `await validateModel(model, schema)` перед submit/шагом | submit/validate НЕ запускают ambient-схему валидации |
1734
1974
  | `FieldPath`, `FieldPathNode` | `model.$.field` (`PathAwareSignal`) | Пути заменены сигналами |
1735
1975
  | `ctx.form.x.value.value` | `model.x` / `model.$.x.value` | В behaviors читаем модель напрямую |
1736
1976
  | `ctx.setFieldValue(name, value)` | `model.x = value` / `compute(...)` | Не существует |
1737
1977
  | `transformers`, `createTransformer` | `transformValue(signal, fn)` | Готового набора трансформеров нет |
1738
- | `equalTo`, `custom`, `notEmpty`, `date` (validators) | кастомный `ModelValidator` | Таких фабрик нет; пиши функцию `(value, scope, root)` |
1739
1978
  | `useHiddenCondition` | `useFormControlValue` + условный рендер в JSX | Хука нет |
1740
1979
  | `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
1741
1980
  | `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |
@@ -1743,33 +1982,49 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
1743
1982
  ### Common Import Errors
1744
1983
 
1745
1984
  ```typescript
1746
- // WRONG - эти символы НЕ существуют
1747
- import { useForm, validateForm } from '@reformer/core'; // NO!
1748
- import { validate, applyWhen, equalTo } from '@reformer/core/validators'; // NO!
1749
- import { transformers } from '@reformer/core/behaviors'; // NO!
1750
- import type { FieldPath, ValidationSchemaFn } from '@reformer/core'; // NO!
1985
+ // WRONG - этих символов / путей НЕ существует
1986
+ import { useForm, validateForm, validateFormModel } from '@reformer/core'; // NO!
1987
+ import { validate, applyWhen, equalTo } from '@reformer/core/validators'; // NO! операторов тут нет
1988
+ import { transformers } from '@reformer/core/behaviors'; // NO!
1989
+ import type { FieldPath, ValidationSchemaFn } from '@reformer/core'; // NO!
1751
1990
 
1752
1991
  // CORRECT
1753
- import { createModel, createForm, validateFormModel, useFormControl } from '@reformer/core';
1754
- import { required, email, min } from '@reformer/core/validators';
1755
- import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';
1756
- import type { FieldConfig, FieldNode, ModelValidator } from '@reformer/core';
1992
+ import { createModel, createForm, useFormControl } from '@reformer/core';
1993
+ import type { FormModel, ValidationError, FieldConfig } from '@reformer/core';
1994
+ // Операторы валидации + раннер + defineValidationSchema — отдельный сабпуть:
1995
+ import {
1996
+ validate, validateAsync, validateWhen, cross, each, apply,
1997
+ defineValidationSchema, validateModel,
1998
+ type Rule, type AsyncRule, type ValidationSchema,
1999
+ } from '@reformer/core/validation';
2000
+ // Фабрики-валидаторы (value-only, принимают nullable):
2001
+ import { required, email, min, minLength, pattern } from '@reformer/core/validators';
2002
+ // Поведение (отдельный слой, контракт не менялся):
2003
+ import { defineFormBehavior, compute, onChange, revalidateWhen } from '@reformer/core/behaviors';
1757
2004
  ```
1758
2005
 
1759
2006
  ### Schema Common Mistakes
1760
2007
 
2008
+ Валидация и layout — **разные** контракты. Layout-узел привязывает поле к сигналу и
2009
+ НЕ несёт `validators`; правила живут в отдельной `defineValidationSchema`.
2010
+
1761
2011
  ```typescript
1762
- // WRONG - примитивные значения / value-литерал не работают под M1
2012
+ // WRONG - примитив/литерал вместо сигнала, и validators прямо в layout-узле
1763
2013
  const schema = {
1764
- name: '', // нет привязки к сигналу
1765
- email: { value: '', ... }, // value должен быть model.$.email, а не литерал
2014
+ name: '', // нет привязки к сигналу
2015
+ email: { value: '', validators: [required()] }, // value — литерал, а validators в layout больше нет
1766
2016
  };
1767
2017
 
1768
- // CORRECT - каждое поле привязано к сигналу модели
1769
- const schema = {
2018
+ // CORRECT - layout только связывает поле с сигналом модели...
2019
+ const layout = {
1770
2020
  name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
1771
2021
  email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
1772
2022
  };
2023
+
2024
+ // ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
2025
+ const validation = defineValidationSchema<Form>(({ model }) => {
2026
+ validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);
2027
+ });
1773
2028
  ```
1774
2029
 
1775
2030
  ### Behaviors Common Mistakes
@@ -1782,6 +2037,14 @@ enableWhen(path.city, (form) => Boolean(form.country));
1782
2037
  enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
1783
2038
  ```
1784
2039
 
2040
+ Поведение **не владеет** валидацией. Чтобы поведение инициировало прогон схемы — мост
2041
+ `revalidateWhen` (validate/submit сами схему не запускают):
2042
+
2043
+ ```typescript
2044
+ // CORRECT - поведение дёргает внешний раннер валидации при изменении зависимости
2045
+ revalidateWhen([model.$.password], () => void validateModel(model, validation));
2046
+ ```
2047
+
1785
2048
  ## 19. Purpose
1786
2049
 
1787
2050
  «Показать поле X только если Y» под M1 решается тремя независимыми механизмами — выбирай по
@@ -1789,12 +2052,13 @@ enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true })
1789
2052
 
1790
2053
  1. **Видимость / доступность** (поле остаётся в модели, но выключается/включается и не валидируется)
1791
2054
  — behaviors: `enableWhen`, а для условной записи значения — `compute` / `copyFrom` с опцией `{ when }`.
1792
- 2. **Условная валидация** (правила применяются только в истинной ветке) — нативный branch-узел
1793
- схемы `{ when, children }`, исполняется `validateFormModel`.
2055
+ 2. **Условная валидация** (правила применяются только в истинной ветке) — оператор
2056
+ `validateWhen(() => cond, () => {…})` внутри схемы `defineValidationSchema`, прогоняется раннером `validateModel`.
1794
2057
  3. **Скрытие из разметки** (узел вообще не рендерится) — `useFormControlValue` + условный рендер в JSX.
1795
2058
 
1796
- > Ни `applyWhen`, ни `ValidationSchemaFn` **не экспортируются** из `@reformer/core` — см. раздел
1797
- > [Не экспорт (частая ошибка)](#не-экспорт-частая-ошибка) и `17-nonexistent-api.md`.
2059
+ > Условная **валидация** — оператор `validateWhen` из `@reformer/core/validation`; старого branch-узла
2060
+ > `{ when, children }` и хелпера `applyWhen` больше нет — см.
2061
+ > [Anti-patterns удалённого контракта валидации](#anti-patterns-удалённого-контракта-валидации) и `17-nonexistent-api.md`.
1798
2062
 
1799
2063
  ## 20. Видимость и доступность
1800
2064
 
@@ -1850,35 +2114,42 @@ export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
1850
2114
 
1851
2115
  ## 21. Условная валидация
1852
2116
 
1853
- Правила, действующие только при выполнении условия, задаются **нативным branch-узлом**
1854
- `{ when: (scope, root) => boolean, children: [...] }` прямо в дереве схемы. `validateFormModel`
1855
- обходит дерево: при истинном `when` валидирует `children`, при ложном — **поддерево пропускается,
1856
- а ошибки его полей очищаются** (движок вызывает `setErrors([])`). `scope` — ближайшая под-модель
1857
- (элемент массива или корень), `root` — корневая модель формы.
2117
+ Правила, действующие только при выполнении условия, оборачиваются оператором
2118
+ `validateWhen(() => cond, () => {…})` внутри схемы `defineValidationSchema<T>(({ model }) => …)`.
2119
+ Условие `cond` читает модель напрямую (`model.field` — снимок на момент прогона): при истинном условии
2120
+ раннер применяет вложенные `validate`/`cross`/…, при ложном — правила ветки **гасятся** (поля,
2121
+ затронутые ранее, получают `setErrors([])`). Прогон — внешним раннером
2122
+ `validateModel(model, schema): Promise<boolean>` (ошибки сами роутятся в ноды формы).
1858
2123
 
1859
2124
  ```typescript
1860
- import { validateFormModel } from '@reformer/core';
2125
+ import {
2126
+ validate,
2127
+ validateWhen,
2128
+ defineValidationSchema,
2129
+ validateModel,
2130
+ } from '@reformer/core/validation';
1861
2131
  import { required, min } from '@reformer/core/validators';
1862
2132
 
1863
- const schema = {
1864
- children: [
1865
- { value: model.$.loanType, validators: [required()] },
1866
- {
1867
- // ветка применяется только для ипотеки
1868
- when: (_scope, root) => root.loanType === 'mortgage',
1869
- children: [
1870
- { value: model.$.propertyValue, validators: [required(), min(1_000_000)] },
1871
- { value: model.$.initialPayment, validators: [required()] },
1872
- ],
2133
+ const schema = defineValidationSchema<CreditForm>(({ model }) => {
2134
+ validate(model.$.loanType, [required()]);
2135
+
2136
+ // ветка применяется только для ипотеки
2137
+ validateWhen(
2138
+ () => model.loanType === 'mortgage',
2139
+ () => {
2140
+ validate(model.$.propertyValue, [required(), min(1_000_000)]);
2141
+ validate(model.$.initialPayment, [required()]);
1873
2142
  },
1874
- ],
1875
- };
2143
+ );
2144
+ });
1876
2145
 
1877
- const { valid, errors } = await validateFormModel(model, schema);
2146
+ const ok: boolean = await validateModel(model, schema);
1878
2147
  ```
1879
2148
 
1880
2149
  Переключение `loanType` с `mortgage` на другой тип автоматически снимает ошибки `required` с
1881
- `propertyValue`/`initialPayment` — их держать enabled и не валидировать помогает `enableWhen` выше.
2150
+ `propertyValue`/`initialPayment` — при ложном условии ветка `validateWhen` гасит их (`setErrors([])`).
2151
+ `validateWhen` управляет только **валидацией**; чтобы поля ещё и выпадали из состояния (сброс
2152
+ значения), держи их под `enableWhen` из раздела выше — слои независимы.
1882
2153
 
1883
2154
  ## 22. Скрытие в JSX
1884
2155
 
@@ -1902,42 +2173,57 @@ function MortgageFields({ form }: { form: CreditForm }) {
1902
2173
  ```
1903
2174
 
1904
2175
  Скрытие в JSX — чисто визуальное: если поле должно ещё и **выпадать из валидации/состояния**,
1905
- сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или branch-узлом (валидация).
2176
+ сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или `validateWhen` (валидация).
1906
2177
 
1907
- ## 23. Не экспорт (частая ошибка)
2178
+ ## 23. Anti-patterns удалённого контракта валидации
1908
2179
 
1909
- - **`applyWhen` — это ЛОКАЛЬНЫЙ typed-хелпер примера, а не экспорт `@reformer/core`.** Он лишь
1910
- эмитит нативный branch-узел `{ when, children }`. Каноничное определение (см.
1911
- `complex-multy-step-form/schemas/validation.ts`):
2180
+ Условную валидацию раньше собирали иначе; эти формы удалены при переходе на `@reformer/core/validation`.
2181
+ Ниже — **как НЕ надо** (чтобы не регрессировать); рабочий вариант — `validateWhen` из раздела выше.
2182
+
2183
+ - **Нет branch-узла `{ when, children }` в дереве схемы.** Старая схема была деревом
2184
+ `{ value, validators: [...] }` с ветками `{ when: (scope, root) => boolean, children: [...] }`,
2185
+ которое обходил `validateFormModel`. Теперь схема — функция `({ model }) => void`, а условие —
2186
+ оператор `validateWhen(() => cond, () => {…})`; отдельного объекта-узла нет.
1912
2187
 
1913
2188
  ```typescript
1914
- // локальный сахар в самом примере — НЕ импорт из библиотеки
1915
- const applyWhen = (cond: (form: Root) => boolean, children: SchemaNode[]): SchemaNode => ({
1916
- when: (_scope: unknown, root: unknown) => cond(root as Root),
1917
- children,
1918
- });
2189
+ // УДАЛЕНО — так больше нельзя
2190
+ const schema = {
2191
+ children: [
2192
+ { value: model.$.loanType, validators: [required()] },
2193
+ { when: (_s, root) => root.loanType === 'mortgage', children: [ /* … */ ] },
2194
+ ],
2195
+ };
2196
+ await validateFormModel(model, schema); // символа нет
1919
2197
  ```
1920
2198
 
1921
- Публичный API — сам `validateFormModel(model, schema)` и форма узла `{ when, children }`.
1922
- Не пиши `import { applyWhen } from '@reformer/core'` — такого символа нет.
2199
+ - **`applyWhen` не существует** — ни как экспорт, ни как локальный сахар: он эмитил branch-узел,
2200
+ которого больше нет. Оборачивай условные правила оператором `validateWhen`.
2201
+
2202
+ - **`ValidationSchemaFn` удалён** (тип старой path-схемы). Тип схемы — `ValidationSchema<T>`
2203
+ (`(ctx: { model: FormModel<T> }) => void`), фабрика-обёртка — `defineValidationSchema<T>(fn)`.
1923
2204
 
1924
- - **`ValidationSchemaFn` не существует** (тип старой path-схемы удалён при переходе на M1). Тип
1925
- кастомного/cross-field валидатора — **`ModelValidator<TValue, TModel, TRoot>`**:
2205
+ - **Cross-field — обычная функция над снапшотом, а не `ModelValidator (value, scope, root)`.**
2206
+ Соседние поля читаются из снимка модели (`f: Root`, эквивалент `model.get()`), а правило навешивается
2207
+ оператором `cross(sig, fn)`. Каноничное использование — `complex-multy-step-form/schemas/validation.ts`.
1926
2208
 
1927
2209
  ```typescript
1928
- import type { ModelValidator } from '@reformer/core';
2210
+ import { cross } from '@reformer/core/validation';
2211
+ import type { ValidationError } from '@reformer/core';
1929
2212
 
1930
- // (value, scope, root) — cross-field читает соседей через root
1931
- const passwordsMatch: ModelValidator<string, unknown, { password: string }> = (value, _scope, root) =>
1932
- value && root.password && value !== root.password
2213
+ // (f: Root) => error — сравнение с соседним полем, без scope/root-параметров
2214
+ const passwordsMatch = (f: { password: string; confirmPassword: string }): ValidationError | null =>
2215
+ f.confirmPassword && f.password && f.confirmPassword !== f.password
1933
2216
  ? { code: 'mismatch', message: 'Пароли не совпадают' }
1934
2217
  : null;
2218
+
2219
+ // внутри defineValidationSchema — fn получает снапшот текущего scope (model.get()):
2220
+ cross(model.$.confirmPassword, passwordsMatch);
1935
2221
  ```
1936
2222
 
1937
2223
  ## 24. See also
1938
2224
 
1939
- - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom`, branch-узел, `ModelValidator`
1940
- - [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, …)
2225
+ - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom` (поведение) и `validate`/`validateWhen`/`cross`/`validateModel` (валидация)
2226
+ - [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, `validateFormModel`, …)
1941
2227
  - [25-reset-when.md](./25-reset-when.md) — `resetWhen` как альтернатива, когда поле остаётся enabled
1942
2228
  - [19-reading-values.md](./19-reading-values.md) — `useFormControlValue` и чтение значений в React
1943
2229
 
@@ -2235,17 +2521,23 @@ aggregateInto(model.$.rows, (rows) => {
2235
2521
 
2236
2522
  ### Array Cross-Validation
2237
2523
 
2238
- Cross-field правило по массиву — `ModelValidator`, читает элементы через `root`:
2524
+ Whole-array правило — оператор `cross(sig, (f) => ...)` из `@reformer/core/validation`:
2525
+ `f` — снапшот `model.get()`, ошибка вешается на скалярное поле-носитель `sig`. Per-item
2526
+ правила — `each(model.<array>, (im) => ...)`.
2239
2527
 
2240
2528
  ```typescript
2241
- import type { ModelValidator } from '@reformer/core';
2529
+ import { cross } from '@reformer/core/validation';
2530
+ import type { ValidationError } from '@reformer/core';
2242
2531
 
2243
- const percentagesSumTo100: ModelValidator<unknown, unknown, MyForm> = (_v, _s, root) => {
2244
- const total = root.items.reduce((sum, i) => sum + (i.percentage || 0), 0);
2532
+ const percentagesSumTo100 = (f: MyForm): ValidationError | null => {
2533
+ const total = f.items.reduce((sum, i) => sum + (i.percentage || 0), 0);
2245
2534
  return Math.abs(total - 100) > 0.01
2246
2535
  ? { code: 'invalid_total', message: 'Percentages must sum to 100%' }
2247
2536
  : null;
2248
2537
  };
2538
+
2539
+ // внутри defineValidationSchema<MyForm>(({ model }) => { ... })
2540
+ cross(model.$.totalPercent, percentagesSumTo100); // носитель — скалярное поле формы
2249
2541
  ```
2250
2542
 
2251
2543
  ## 28. Как устроено под M1
@@ -2840,12 +3132,14 @@ A: Симптом частых записей. Форматирование с
2840
3132
  `revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
2841
3133
  зависимостей. Применяется, когда правило зависит от значений других полей
2842
3134
  (`amount <= maxAmount`, `confirmPassword === password`, `initialPayment >= propertyValue * 0.2`).
2843
- Под M1 валидация on-demand (`validateFormModel`), поэтому ревалидация выражается явным
2844
- колбэком, а не автоматической привязкой к полю.
3135
+ Валидация — отдельная функция-схема (`@reformer/core/validation`), прогоняемая по требованию через
3136
+ `validateModel(model, schema)`. Поэтому ревалидация выражается явным колбэком, а не автоматической
3137
+ привязкой к полю: поведение (`revalidateWhen`) лишь _инициирует_ прогон схемы валидации при изменении зависимости.
2845
3138
 
2846
3139
  ## 57. API
2847
3140
 
2848
- Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
3141
+ `revalidateWhen` — оператор поведения (`@reformer/core/behaviors`, либо примитив `@reformer/core`).
3142
+ Его сигнатура НЕ зависит от контракта валидации:
2849
3143
 
2850
3144
  ```typescript
2851
3145
  // примитив: возвращает cleanup
@@ -2856,8 +3150,8 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
2856
3150
  ```
2857
3151
 
2858
3152
  `deps` — массив сигналов-триггеров (`model.$.field`). `revalidate` — колбэк (обычно
2859
- `() => validateFormModel(model, schema)`). Вызывается при изменении любой зависимости
2860
- (НЕ на инициализации), вне effect-контекста.
3153
+ `() => void validateModel(model, schema)`, где `schema` — `defineValidationSchema<T>(({ model }) => …)`).
3154
+ Вызывается при изменении любой зависимости (НЕ на инициализации), вне effect-контекста.
2861
3155
 
2862
3156
  ## 58. Examples
2863
3157
 
@@ -2865,27 +3159,29 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
2865
3159
 
2866
3160
  ```typescript
2867
3161
  import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
2868
- import { validateFormModel } from '@reformer/core';
3162
+ import { validateModel } from '@reformer/core/validation';
2869
3163
 
2870
3164
  export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
2871
3165
  revalidateWhen([model.$.maxAmount], () => {
2872
- void validateFormModel(model, paymentSchema);
3166
+ void validateModel(model, paymentSchema);
2873
3167
  });
2874
3168
  });
2875
3169
  ```
2876
3170
 
2877
- Пример: `revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema))`.
3171
+ Пример: `revalidateWhen([model.$.maxAmount], () => void validateModel(model, paymentSchema))`.
3172
+ Схема `paymentSchema` — стабильный module-level `const` (важно для отмены устаревших прогонов
3173
+ в `validateModel`), где правило amount навешано через `cross(model.$.amount, amountVsMax)`.
2878
3174
 
2879
3175
  ### Несколько триггеров
2880
3176
 
2881
3177
  ```typescript
2882
3178
  import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
2883
- import { validateFormModel } from '@reformer/core';
3179
+ import { validateModel } from '@reformer/core/validation';
2884
3180
 
2885
3181
  export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) => {
2886
3182
  // initialPayment зависит и от propertyValue, и от loanAmount
2887
3183
  revalidateWhen([model.$.propertyValue, model.$.loanAmount], () => {
2888
- void validateFormModel(model, mortgageSchema);
3184
+ void validateModel(model, mortgageSchema);
2889
3185
  });
2890
3186
  });
2891
3187
  ```
@@ -2894,13 +3190,14 @@ export const mortgageBehavior = defineFormBehavior<MortgageForm>(({ model }) =>
2894
3190
 
2895
3191
  ```typescript
2896
3192
  import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
2897
- import { validateFormModel } from '@reformer/core';
3193
+ import { validateModel } from '@reformer/core/validation';
2898
3194
 
2899
- // правило совпадения — cross-field ModelValidator на confirmPassword (см. 03-api-signatures.md)
3195
+ // правило совпадения — cross-field в схеме: cross(model.$.confirmPassword, passwordsMatch),
3196
+ // где passwordsMatch читает снапшот формы (см. 03-api-signatures.md)
2900
3197
  export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ model }) => {
2901
3198
  // при смене password перевалидируем схему (confirm перепроверится)
2902
3199
  revalidateWhen([model.$.password], () => {
2903
- void validateFormModel(model, registrationSchema);
3200
+ void validateModel(model, registrationSchema);
2904
3201
  });
2905
3202
  });
2906
3203
  ```
@@ -2908,55 +3205,68 @@ export const registrationBehavior = defineFormBehavior<RegistrationForm>(({ mode
2908
3205
  ### Как примитив (вне defineFormBehavior)
2909
3206
 
2910
3207
  ```typescript
2911
- import { revalidateWhen, validateFormModel } from '@reformer/core';
2912
- const stop = revalidateWhen([model.$.maxAmount], () => void validateFormModel(model, schema));
3208
+ import { revalidateWhen } from '@reformer/core';
3209
+ import { validateModel } from '@reformer/core/validation';
3210
+
3211
+ const stop = revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
2913
3212
  ```
2914
3213
 
2915
3214
  ## 59. Anti-patterns
2916
3215
 
2917
3216
  ```typescript
2918
3217
  // ❌ Триггер == поле, которое и так меняется само
2919
- revalidateWhen([model.$.amount], () => validateFormModel(model, schema));
3218
+ revalidateWhen([model.$.amount], () => validateModel(model, schema));
2920
3219
  // amount и так валидируется при собственном изменении в общем прогоне
2921
3220
 
2922
3221
  // ✅ Триггеры — ДРУГИЕ поля, от которых зависит правило amount
2923
- revalidateWhen([model.$.maxAmount, model.$.discount], () => validateFormModel(model, schema));
3222
+ revalidateWhen([model.$.maxAmount, model.$.discount], () => validateModel(model, schema));
2924
3223
  ```
2925
3224
 
2926
3225
  ```typescript
2927
3226
  // ❌ Правило только через revalidateWhen, но в схеме нет зависимости от триггера
2928
- revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
3227
+ revalidateWhen([model.$.maxAmount], () => validateModel(model, schema));
2929
3228
  // а валидатор amount — статичный max(1000), не читает maxAmount
2930
3229
 
2931
- // ✅ Сначала cross-field ModelValidator, читающий root, потом revalidateWhen
2932
- const amountVsMax: ModelValidator<number, unknown, Form> = (v, _s, root) =>
2933
- v != null && root.maxAmount != null && v > root.maxAmount ? { code: 'tooBig', message: '...' } : null;
2934
- // amount: { value: model.$.amount, validators: [amountVsMax] }
2935
- revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
3230
+ // ✅ Сначала cross-field правило (читает снапшот формы) в схеме валидации, потом revalidateWhen
3231
+ const amountVsMax = (f: Form): ValidationError | null =>
3232
+ f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
3233
+ ? { code: 'tooBig', message: '...' }
3234
+ : null;
3235
+
3236
+ const schema = defineValidationSchema<Form>(({ model }) => {
3237
+ cross(model.$.amount, amountVsMax); // навешиваем правило на поле amount
3238
+ });
3239
+ revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
2936
3240
  ```
2937
3241
 
2938
3242
  ## 60. Troubleshooting
2939
3243
 
2940
3244
  **Q: Ошибка target не пропадает после изменения триггера.**
2941
- A: Проверьте, что (1) правило реально читает значение триггера через `root` (cross-field
2942
- `ModelValidator`); (2) в `deps` передан именно сигнал (`model.$.trigger`); (3) колбэк вызывает
2943
- `validateFormModel` (роутит ошибки в ноды).
3245
+ A: Проверьте, что (1) правило реально читает значение триггера — cross-field `cross(sig, fn)`,
3246
+ где `fn` берёт снапшот формы (`model.get()` / `f`); (2) в `deps` передан именно сигнал (`model.$.trigger`);
3247
+ (3) колбэк вызывает `validateModel` (роутит ошибки в ноды через `getNodeForSignal(sig).setErrors`).
2944
3248
 
2945
3249
  **Q: Как перевалидировать только одно поле, а не всю схему?**
2946
- A: Передай в колбэк под-схему только этого поля: `validateFormModel(model, { children: [fieldNode] })`.
3250
+ A: Схема — обычная функция, поэтому опиши стабильную под-схему только для этого поля и прогони её:
3251
+ `validateModel(model, oneFieldSchema)`, где
3252
+ `const oneFieldSchema = defineValidationSchema<Form>(({ model }) => cross(model.$.amount, amountVsMax))`.
3253
+ Раннер выставит/погасит ошибки только затронутых полей (гашение накапливается на пару `(model, schema)`).
2947
3254
 
2948
3255
  ## 61. See also
2949
3256
 
2950
- - [03-api-signatures.md](./03-api-signatures.md) — cross-field `ModelValidator` и `validateFormModel`
3257
+ - [03-api-signatures.md](./03-api-signatures.md) — cross-field `cross(sig, fn)` и раннер `validateModel`
2951
3258
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
2952
3259
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения
2953
3260
 
2954
3261
  ## 62. Purpose
2955
3262
 
2956
- Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateFormModel`) →
2957
- проверить `result.valid` → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
2958
- Валидация — чистая функция ДАННЫХ (модели), она же роутит ошибки в ноды формы, поэтому UI
2959
- подсветит проблемные поля. `model.reset()` возвращает значения к initial-снимку.
3263
+ Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateModel`) →
3264
+ проверить `boolean`-результат → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
3265
+ Валидация — **отдельный слой** (`@reformer/core/validation`, схема через `defineValidationSchema`), она
3266
+ НЕ входит в layout-дерево, которое получает `createForm`. Раннер `validateModel(model, schema)` сам роутит
3267
+ ошибки в ноды формы, поэтому UI подсветит проблемные поля, а наружу вернёт лишь `boolean`
3268
+ (false = есть блокирующая ошибка; `severity:'warning'` показывается, но submit не блокирует).
3269
+ `model.reset()` возвращает значения к initial-снимку.
2960
3270
 
2961
3271
  ## 63. API
2962
3272
 
@@ -2971,13 +3281,23 @@ interface ModelApi<T> {
2971
3281
  captureInitial(): void; // зафиксировать текущие как новый initial
2972
3282
  }
2973
3283
 
2974
- // Валидация данных (headless, роутит ошибки в ноды формы):
2975
- validateFormModel<T>(model, schema): Promise<{ valid: boolean; errors: Record<string, ValidationError[]> }>
3284
+ // Валидация данных — headless-раннер: находит ноды по сигналам модели, роутит в них ошибки,
3285
+ // отменяет устаревшие прогоны той же (model, schema). Возвращает ТОЛЬКО boolean:
3286
+ validateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>): Promise<boolean>
3287
+ // true = нет блокирующих ошибок (severity:'warning' не блокирует, но показывается).
3288
+ // false = есть блокирующая ошибка; она уже проставлена в ноду → UI подсветит поле.
2976
3289
 
2977
- // Нода поля (для точечной работы с UI-состоянием):
2978
- form.<field>.setErrors([{ code, message }]);
2979
- form.<field>.markAsTouched();
2980
- form.<field>.reset();
3290
+ // Схема валидации — отдельный слой над моделью (не смешивается с layout):
3291
+ defineValidationSchema<T>(({ model }) => {
3292
+ validate(model.$.field, [rules]); // синхронные правила поля
3293
+ // validateAsync / validateWhen / cross / each / apply — см. contract-spec
3294
+ }): ValidationSchema<T>
3295
+
3296
+ // Нода поля/формы (для точечной работы с UI-состоянием):
3297
+ form.markAsTouched(); // тронуть все поля (показать ошибки)
3298
+ form.clearErrors(); // снять ошибки со всех нод
3299
+ form.<field>.setErrors([{ code, message }]); // серверная/бизнес-ошибка в конкретное поле
3300
+ form.<field>.clearErrors();
2981
3301
  ```
2982
3302
 
2983
3303
  ## 64. Examples
@@ -2986,23 +3306,34 @@ form.<field>.reset();
2986
3306
 
2987
3307
  ```tsx
2988
3308
  import { useMemo } from 'react';
2989
- import { createModel, createForm, validateFormModel } from '@reformer/core';
3309
+ import { createModel, createForm } from '@reformer/core';
3310
+ import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
3311
+ import { required, email, minLength } from '@reformer/core/validators';
2990
3312
 
2991
3313
  type RegistrationFormData = { username: string; email: string; password: string };
2992
3314
 
3315
+ // Валидация — отдельная схема над моделью. НЕ входит в layout, который получает createForm.
3316
+ // Стабильная module-level `const`-ссылка — чтобы validateModel мог отменять устаревшие прогоны.
3317
+ const registrationSchema = defineValidationSchema<RegistrationFormData>(({ model }) => {
3318
+ validate(model.$.username, [required({ message: 'Имя обязательно' }), minLength(3)]);
3319
+ validate(model.$.email, [required({ message: 'Email обязателен' }), email()]);
3320
+ validate(model.$.password, [required({ message: 'Пароль обязателен' }), minLength(8)]);
3321
+ });
3322
+
2993
3323
  function RegistrationForm() {
2994
- const { model, form, schema } = useMemo(() => {
3324
+ const { model, form } = useMemo(() => {
2995
3325
  const m = createModel<RegistrationFormData>({ username: '', email: '', password: '' });
2996
- const s = buildSchema(m);
2997
- return { model: m, form: createForm({ model: m, schema: s }), schema: s };
3326
+ // schema здесь — layout-дерево нод (компоненты/раскладка), без валидаторов.
3327
+ return { model: m, form: createForm({ model: m, schema: buildLayout(m) }) };
2998
3328
  }, []);
2999
3329
 
3000
3330
  const handleSubmit = async (e: React.FormEvent) => {
3001
3331
  e.preventDefault();
3332
+ form.markAsTouched(); // показать ошибки на всех полях
3002
3333
 
3003
- // Шаг 1: валидация данных (ошибки автоматически проставятся в ноды → UI подсветит)
3004
- const result = await validateFormModel(model, schema);
3005
- if (!result.valid) return;
3334
+ // Шаг 1: прогон валидации данных. Ошибки автоматически проставятся в ноды → UI подсветит.
3335
+ const valid = await validateModel(model, registrationSchema);
3336
+ if (!valid) return;
3006
3337
 
3007
3338
  // Шаг 2: достать чистые данные и отправить
3008
3339
  const payload = model.get();
@@ -3032,9 +3363,10 @@ function RegistrationForm() {
3032
3363
  ```tsx
3033
3364
  async function handleSubmit(e: React.FormEvent) {
3034
3365
  e.preventDefault();
3366
+ form.markAsTouched();
3035
3367
 
3036
- const result = await validateFormModel(model, schema);
3037
- if (!result.valid) return;
3368
+ const valid = await validateModel(model, registrationSchema);
3369
+ if (!valid) return;
3038
3370
 
3039
3371
  try {
3040
3372
  const response = await api.register(model.get());
@@ -3078,39 +3410,61 @@ function ActionButtons({ model }: { model: FormModel<RegistrationFormData> }) {
3078
3410
 
3079
3411
  ### Использование вне React (server action / Node)
3080
3412
 
3081
- Валидация headless — работает без UI/нод:
3413
+ Раннер headless — работает без UI-компонентов, только с моделью и схемой:
3082
3414
 
3083
3415
  ```typescript
3084
- import { createModel, validateModel } from '@reformer/core';
3416
+ import { createModel } from '@reformer/core';
3417
+ import { validateModel } from '@reformer/core/validation';
3085
3418
 
3086
3419
  async function processFormPayload(rawData: Partial<MyForm>) {
3087
3420
  const model = createModel<MyForm>(initialValues);
3088
3421
  model.patch(rawData); // частичный load данных
3089
3422
 
3090
- const result = await validateModel(model, schema); // без нод — просто данные
3091
- if (!result.valid) return { ok: false, errors: result.errors };
3092
- return { ok: true, data: model.get() };
3423
+ // validateModel находит ноды по сигналам модели и роутит в них ошибки, возвращая boolean.
3424
+ const valid = await validateModel(model, myFormSchema);
3425
+ if (!valid) return { ok: false as const };
3426
+ return { ok: true as const, data: model.get() };
3093
3427
  }
3094
3428
  ```
3095
3429
 
3096
3430
  ## 65. Anti-patterns
3097
3431
 
3098
3432
  ```typescript
3099
- // ❌ Чтение результата валидации без await
3433
+ // ❌ Обращение к .valid: раннер возвращает boolean, а не { valid, errors }
3434
+ const result = await validateModel(model, schema);
3435
+ if (!result.valid) return; // result — boolean, .valid === undefined → guard никогда не сработает
3436
+
3437
+ // ✅ Результат — сам boolean
3438
+ const valid = await validateModel(model, schema);
3439
+ if (!valid) return;
3440
+ ```
3441
+
3442
+ ```typescript
3443
+ // ❌ Чтение результата валидации без await — сабмитим по неполному вердикту
3100
3444
  const handleSubmit = (e) => {
3101
3445
  e.preventDefault();
3102
- validateFormModel(model, schema); // не дождались async-валидаторов
3446
+ validateModel(model, schema); // Promise проигнорирован, async-валидаторы не дождались
3103
3447
  submit(model.get());
3104
3448
  };
3105
3449
 
3106
- // ✅ Дождаться и проверить result.valid
3450
+ // ✅ Дождаться и проверить boolean
3107
3451
  const handleSubmit = async (e) => {
3108
3452
  e.preventDefault();
3109
- const result = await validateFormModel(model, schema);
3110
- if (result.valid) submit(model.get());
3453
+ if (await validateModel(model, schema)) submit(model.get());
3111
3454
  };
3112
3455
  ```
3113
3456
 
3457
+ ```typescript
3458
+ // ❌ Валидаторы в layout-дереве, отдаваемом в createForm — layout не несёт правил
3459
+ createForm({ model, schema: { username: { value: '', validators: [required()] } } });
3460
+
3461
+ // ✅ Валидация — отдельная схема, прогоняется раннером
3462
+ const schema = defineValidationSchema<T>(({ model }) => {
3463
+ validate(model.$.username, [required()]);
3464
+ });
3465
+ await validateModel(model, schema);
3466
+ ```
3467
+
3114
3468
  ```typescript
3115
3469
  // ❌ model.reset() до ответа сервера — потеря данных при ошибке
3116
3470
  await api.send(model.get());
@@ -3137,7 +3491,13 @@ model.reset();
3137
3491
 
3138
3492
  **Q: После `reset()` в UI остались старые ошибки.**
3139
3493
  A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
3140
- `validateFormModel(model, schema)` после reset, либо очисти точечно `form.field.setErrors([])`.
3494
+ `validateModel(model, schema)` после reset (валидные поля погаснут сами), либо очисти напрямую —
3495
+ `form.clearErrors()` / `form.<field>.clearErrors()`.
3496
+
3497
+ **Q: `validateModel` вернул `true`, но в поле висит ошибка.**
3498
+ A: Это правило с `severity: 'warning'` — оно показывается, но submit не блокирует, поэтому раннер
3499
+ и вернул `true`. Блокируют только ошибки без `severity` (default). Чтобы не пускать submit при
3500
+ warning — проверяй его отдельно, вне `validateModel`.
3141
3501
 
3142
3502
  **Q: `reset()` не возвращает данные, загруженные с сервера.**
3143
3503
  A: `reset()` возвращает к initial-снимку (значения на момент `createModel`). `set/patch` НЕ
@@ -3151,8 +3511,9 @@ A: `model.get()` возвращает значения всех полей. Фи
3151
3511
  ## 67. See also
3152
3512
 
3153
3513
  - [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
3154
- - [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateFormModel`
3155
- - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateFormModel`
3514
+ - [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateModel` (`validateStep`/`validateAll`)
3515
+ - [27-revalidate-when.md](./27-revalidate-when.md) — мост «поведение → валидация»: `revalidateWhen([deps], () => void validateModel(model, schema))`
3516
+ - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateModel`
3156
3517
  - [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки
3157
3518
 
3158
3519
  ## 68. Purpose
@@ -3330,7 +3691,7 @@ useEffect(() => {
3330
3691
  ## 72. Troubleshooting
3331
3692
 
3332
3693
  **Q: После `set`/`patch` не показываются ошибки.**
3333
- A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateFormModel(model, schema)`.
3694
+ A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateModel(model, schema)` (внешний раннер из `@reformer/core/validation`).
3334
3695
 
3335
3696
  **Q: Опции в `<Select>` не появляются после `updateComponentProps`.**
3336
3697
  A: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что компонент подписан через `useFormControl`;
@@ -3353,7 +3714,8 @@ A: `reset()` возвращает к initial-снимку из `createModel`, н
3353
3714
 
3354
3715
  ### Recipe 1 — Imports (root cause prevention)
3355
3716
 
3356
- - Модель/форма/валидация/хуки/типы/**примитивы behaviors** — из `@reformer/core`.
3717
+ - Модель/форма/хуки/типы/**примитивы behaviors** — из `@reformer/core`.
3718
+ - Схема валидации (операторы + раннер `validateModel`) — из `@reformer/core/validation`.
3357
3719
  - Чистые фабрики валидаторов — из `@reformer/core/validators`.
3358
3720
  - Декларативный DSL (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
3359
3721
 
@@ -3361,12 +3723,17 @@ A: `reset()` возвращает к initial-снимку из `createModel`, н
3361
3723
  import {
3362
3724
  createModel,
3363
3725
  createForm,
3364
- validateFormModel,
3365
3726
  type FormModel,
3366
3727
  type FormProxy,
3367
3728
  type ModelSignals,
3368
- type ModelValidator,
3369
3729
  } from '@reformer/core';
3730
+ import {
3731
+ defineValidationSchema,
3732
+ validate,
3733
+ cross,
3734
+ validateModel,
3735
+ type Rule,
3736
+ } from '@reformer/core/validation';
3370
3737
  import { required, min, max, email } from '@reformer/core/validators';
3371
3738
  import { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';
3372
3739
  ```
@@ -3396,38 +3763,40 @@ export type CreditApplicationForm = {
3396
3763
  ### Recipe 3 — Схема привязана к сигналам модели
3397
3764
 
3398
3765
  `value` поля — это сигнал модели (`model.$.field`), а не литерал. Тип поля выводится из сигнала.
3766
+ Layout-схема БЕЗ валидаторов — правила живут в отдельной validation-схеме.
3399
3767
 
3400
3768
  ```typescript
3401
3769
  const model = createModel<CreditApplicationForm>(initial);
3402
3770
 
3403
3771
  const schema = {
3404
- loanAmount: { value: model.$.loanAmount, component: Input, validators: [required(), min(50000)] },
3772
+ loanAmount: { value: model.$.loanAmount, component: Input },
3405
3773
  // вложенная группа — builder, принимающий ModelSignals<Sub>
3406
3774
  personalData: personalDataNodes(model.$.personalData),
3407
3775
  // массив — { array, item }
3408
3776
  properties: { array: model.properties, item: propertyItem },
3409
3777
  };
3778
+
3779
+ // правила — отдельно; типы полей выводятся из сигналов
3780
+ const validation = defineValidationSchema<CreditApplicationForm>(({ model }) => {
3781
+ validate(model.$.loanAmount, [required(), min(50000)]);
3782
+ });
3410
3783
  ```
3411
3784
 
3412
- ### Recipe 4 — Cross-field валидаторы: типизированный `ModelValidator`
3785
+ ### Recipe 4 — Cross-field валидаторы: `cross` над типизированным снапшотом
3413
3786
 
3414
- Правило — `ModelValidator<TField, TScope, TRoot>`. `root` типизирован формой, соседние поля
3415
- читаются без `as`:
3787
+ Правило — обычная функция `(f: Root) => ValidationError | null` над снапшотом `model.get()`.
3788
+ Соседние поля читаются без `as`; ошибка вешается на поле-носитель `sig`:
3416
3789
 
3417
3790
  ```typescript
3418
- import type { ModelValidator } from '@reformer/core';
3791
+ import type { ValidationError } from '@reformer/core';
3419
3792
 
3420
- const initialPaymentVsProperty: ModelValidator<number, unknown, CreditApplicationForm> = (
3421
- _value,
3422
- _scope,
3423
- root
3424
- ) =>
3425
- root.initialPayment && root.propertyValue && root.initialPayment > root.propertyValue
3793
+ const initialPaymentVsProperty = (f: CreditApplicationForm): ValidationError | null =>
3794
+ f.initialPayment && f.propertyValue && f.initialPayment > f.propertyValue
3426
3795
  ? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }
3427
3796
  : null;
3428
3797
 
3429
- // в схеме:
3430
- initialPayment: { value: model.$.initialPayment, component: Input, validators: [initialPaymentVsProperty] };
3798
+ // внутри defineValidationSchema<CreditApplicationForm>(({ model }) => { ... }):
3799
+ cross(model.$.initialPayment, initialPaymentVsProperty);
3431
3800
  ```
3432
3801
 
3433
3802
  ### Recipe 5 — `compute` читает модель напрямую (без аннотаций)
@@ -3476,18 +3845,19 @@ function MyField<T>({ control }: MyFieldProps<T>) {
3476
3845
 
3477
3846
  ### Recipe 8 — Вынос правил в именованные функции
3478
3847
 
3479
- Cross-field и field-правила — именованные `ModelValidator`, схема остаётся плоской:
3848
+ Field-правила — именованные `Rule<T>`, validation-схема остаётся плоской:
3480
3849
 
3481
3850
  ```typescript
3482
- const validateAdultAge: ModelValidator<string> = (value) => {
3851
+ import type { Rule } from '@reformer/core/validation';
3852
+
3853
+ const validateAdultAge: Rule<string | null> = (value) => {
3483
3854
  if (!value) return null;
3484
3855
  const age = new Date().getFullYear() - new Date(value).getFullYear();
3485
3856
  return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;
3486
3857
  };
3487
3858
 
3488
- const schema = {
3489
- birthDate: { value: model.$.birthDate, component: Input, validators: [validateAdultAge] },
3490
- };
3859
+ // внутри defineValidationSchema<MyForm>(({ model }) => { ... }):
3860
+ validate(model.$.birthDate, [validateAdultAge]);
3491
3861
  ```
3492
3862
 
3493
3863
  ### Anti-patterns to avoid
@@ -3501,76 +3871,138 @@ const schema = {
3501
3871
  ## 75. 31. ASYNC VALIDATOR
3502
3872
 
3503
3873
  Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
3504
- async-валидатор это `ModelValidator`, возвращающий `Promise<ValidationError | null>`. Он
3505
- исполняется движком `validateFormModel`/`validateModel` (async-задачи прогоняются параллельно
3506
- через `Promise.all`).
3874
+ async-правило это `AsyncRule<T>` = `(value, { signal }) => Promise<ValidationError | null>`.
3875
+ Оно регистрируется оператором `validateAsync(sig, [asyncRules])` внутри схемы валидации
3876
+ (`@reformer/core/validation`) и исполняется внешним раннером `validateModel` (async-правила
3877
+ прогоняются параллельно через `Promise.all`, раннер их дожидается).
3878
+
3879
+ Слои разделены: layout (`createForm`-схема / JSON) НЕ несёт валидаторов — правила живут в
3880
+ отдельной функции-схеме `defineValidationSchema<T>(({ model }) => …)`.
3507
3881
 
3508
3882
  ```ts
3509
- import { createModel, createForm, validateFormModel, type ModelValidator } from '@reformer/core';
3883
+ import { createModel } from '@reformer/core';
3884
+ import {
3885
+ validate,
3886
+ validateAsync,
3887
+ defineValidationSchema,
3888
+ validateModel,
3889
+ type AsyncRule,
3890
+ } from '@reformer/core/validation';
3510
3891
  import { required, email } from '@reformer/core/validators';
3511
- import { Input } from '@reformer/ui-kit';
3512
3892
 
3513
- // async-валидатор: (value, scope, root) => Promise<ValidationError | null>
3514
- const checkEmailUnique: ModelValidator<string> = async (value) => {
3893
+ // async-правило: (value, { signal }) => Promise<ValidationError | null>.
3894
+ // `signal` — AbortSignal устаревшего прогона: прокинь его в fetch, чтобы отменить in-flight.
3895
+ const checkEmailUnique: AsyncRule<string> = async (value, { signal }) => {
3515
3896
  if (!value) return null; // пусто = валидно (sync `required` отдельно)
3516
3897
  try {
3517
- const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`);
3898
+ const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
3518
3899
  const { available } = (await res.json()) as { available: boolean };
3519
3900
  return available ? null : { code: 'email-taken', message: 'Email уже зарегистрирован' };
3520
3901
  } catch {
3521
- return { code: 'check-failed', message: 'Не удалось проверить email' };
3902
+ return null; // сетевой сбой/отмена НЕ блокирует submit — возвращаем null, а не ошибку
3522
3903
  }
3523
3904
  };
3524
3905
 
3525
3906
  const model = createModel<{ email: string }>({ email: '' });
3526
- const schema = {
3527
- email: {
3528
- value: model.$.email,
3529
- component: Input,
3530
- // sync-фабрики и async-валидатор в одном массиве validators
3531
- validators: [required(), email(), checkEmailUnique],
3532
- },
3533
- };
3534
- const form = createForm({ model, schema });
3907
+
3908
+ // Схема — обычная функция над моделью; sync и async — разными операторами на одном сигнале.
3909
+ const schema = defineValidationSchema<{ email: string }>(({ model }) => {
3910
+ validate(model.$.email, [required(), email()]); // sync-фабрики
3911
+ validateAsync(model.$.email, [checkEmailUnique]); // async-правило
3912
+ });
3913
+
3914
+ // Прогон по требованию (submit / шаг):
3915
+ const ok = await validateModel(model, schema); // Promise<boolean>
3535
3916
  ```
3536
3917
 
3537
3918
  ### Как это исполняется
3538
3919
 
3539
- 1. `validateFormModel(model, schema)` собирает field-задачи и прогоняет их валидаторы.
3540
- 2. Для каждого поля валидаторы выполняются по порядку; async-валидаторы `await`-ятся.
3541
- 3. `validateModelSync(model, schema)` — синхронный вариант: async-валидаторы **пропускаются**
3542
- (для мгновенных проверок без сети).
3543
- 4. Ошибки роутятся в ноды формы (`form.field.errors`), UI подсвечивает поле.
3920
+ 1. `validateModel(model, schema)` открывает ambient-окно и **синхронно** прогоняет схему:
3921
+ `validate`/`validateAsync`/`cross` регистрируют правила своих полей.
3922
+ 2. Sync-правила выполняются сразу; async-правила из `validateAsync` собираются и после закрытия
3923
+ ambient-окна дожидаются параллельно (`Promise.all`) с прокинутым `AbortSignal`.
3924
+ 3. Ошибки роутятся в ноды формы (`getNodeForSignal(sig).setErrors(...)`), UI подсвечивает поле;
3925
+ поля, ставшие валидными, гасятся (`setErrors([])`).
3926
+ 4. Возвращает `Promise<boolean>` — `true`, если нет блокирующих ошибок (`severity:'warning'`
3927
+ не блокирует). Устаревший (отменённый) прогон возвращает `false` — ему нельзя доверять для submit.
3928
+
3929
+ Синхронного варианта у нового контракта нет: раннер один — `validateModel`, и он всегда `async`.
3544
3930
 
3545
- ### Отдельное поле `asyncValidators`
3931
+ ### Sync и async — два оператора
3546
3932
 
3547
- В схеме можно разделить sync и async: `validators: [...]` и `asyncValidators: [...]`. Оба типа
3548
- поддерживаются `FieldConfig`. На практике удобнее держать всё в `validators` — движок сам
3549
- различает sync/async по возвращаемому `Promise`.
3933
+ Разделение sync/async делается не полем схемы, а разными операторами на одном сигнале:
3934
+ `validate(sig, [syncRules])` и `validateAsync(sig, [asyncRules])`. Раннер прогоняет sync-правила
3935
+ инлайн, а async — дожидается. Держи async-правило дешёвым: ранний `if (!value) return null`
3936
+ пропускает сетевой вызов, пока `required()`/формат ещё не пройдены.
3937
+
3938
+ ```ts
3939
+ validate(model.$.inn, [required(), pattern(/^\d{12}$/)]); // формат — sync, мгновенно
3940
+ validateAsync(model.$.inn, [checkInnInRegistry]); // обращение к API — отдельным оператором
3941
+ ```
3550
3942
 
3551
3943
  ### UI integration
3552
3944
 
3553
- `FormField` из `@reformer/ui-kit` показывает индикатор проверки, пока идёт async-валидация
3554
- (`useFormControl(...).pending === true`):
3945
+ `validateModel(...)` возвращает `Promise<boolean>` — индикатор проверки держи вокруг этого
3946
+ `await`. Раннер схемы **не** выставляет per-field `pending` на ноде (он роутит только ошибки через
3947
+ `setErrors`), поэтому спиннер async-валидации — это собственный флаг (обычно form-level, как
3948
+ `ui.pending` в submit-флоу, или локальный state компонента):
3555
3949
 
3556
3950
  ```tsx
3557
- const { pending, errors } = useFormControl(form.email);
3558
- return pending ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
3951
+ const [checking, setChecking] = useState(false);
3952
+
3953
+ const runValidation = async (): Promise<boolean> => {
3954
+ setChecking(true);
3955
+ try {
3956
+ return await validateModel(model, schema);
3957
+ } finally {
3958
+ setChecking(false);
3959
+ }
3960
+ };
3961
+
3962
+ return checking ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
3559
3963
  ```
3560
3964
 
3561
3965
  ### Debounce и отмена
3562
3966
 
3563
- - Валидация запускается on-demand (на submit / шаг / через `revalidateWhen`), а не на каждый
3564
- keystroke — отдельный `debounce` в валидаторе обычно не нужен.
3565
- - Если нужно дебаунсить дорогой async-валидатор относительно частых изменений, вешай его через
3566
- `revalidateWhen([...], () => validateFormModel(...))` и оборачивай запуск в собственный debounce.
3567
- - Cross-field async — обычный `ModelValidator`, читающий соседние поля через `root`.
3967
+ - **Отмена устаревших — раннером, бесплатно.** Быстрый повторный `validateModel(model, schema)`
3968
+ той же пары `(model, schema)` отменяет предыдущий in-flight прогон через `AbortController`;
3969
+ его `AbortSignal` прокинут в `validateAsync`-правила, поэтому `fetch(url, { signal })` рвётся
3970
+ сам. Ручной debounce для КОРРЕКТНОСТИ не нужен — держи схему стабильным `const`
3971
+ (`defineValidationSchema`), иначе отмена не сматчит прогоны по идентичности.
3972
+ - Валидация запускается on-demand (submit / шаг / через `revalidateWhen`), а не на каждый
3973
+ keystroke — отдельный `debounce` в правиле обычно не нужен.
3974
+ - Если нужно дебаунсить дорогой async-валидатор относительно частых изменений — триггерь прогон
3975
+ из поведения через `onChange`, который даёт debounce из коробки:
3976
+
3977
+ ```ts
3978
+ import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
3979
+ import { validateModel } from '@reformer/core/validation';
3980
+
3981
+ export const behavior = defineFormBehavior<{ username: string }>(({ model }) => {
3982
+ onChange(model.$.username, () => void validateModel(model, schema), { debounce: 300 });
3983
+ });
3984
+ ```
3985
+
3986
+ Либо оборачивай в свой debounce колбэк `revalidateWhen([...], () => void validateModel(...))`.
3987
+ - **Async cross-field** — инлайн `validateAsync`-правило, замыкающее `model` и читающее снапшот
3988
+ соседей `model.get()` до первого `await` (у `AsyncRule` нет `root`, только `value` + `signal`;
3989
+ `cross(...)` — синхронный):
3990
+
3991
+ ```ts
3992
+ validateAsync(model.$.email, [
3993
+ async (email, { signal }) => {
3994
+ const { username } = model.get(); // снапшот соседних полей до await
3995
+ const res = await fetch(`/api/check?email=${email}&user=${username}`, { signal });
3996
+ return (await res.json()).ok ? null : { code: 'conflict', message: 'Пара занята' };
3997
+ },
3998
+ ]);
3999
+ ```
3568
4000
 
3569
4001
  ### See also
3570
4002
 
3571
- - [27-revalidate-when.md](27-revalidate-when.md) — перезапуск валидации по триггерам
4003
+ - [27-revalidate-when.md](27-revalidate-when.md) — перезапуск `validateModel` по триггерам
3572
4004
  - [29-async-preload.md](29-async-preload.md) — async preload данных при init формы
3573
- - [03-api-signatures.md](03-api-signatures.md) — `ModelValidator` и `validateFormModel`
4005
+ - [32-async-options-loading.md](32-async-options-loading.md) — `onChange` с debounce + AbortSignal
3574
4006
 
3575
4007
  ## 76. 32. ASYNC OPTIONS LOADING
3576
4008
 
@@ -4134,15 +4566,16 @@ export function createForm<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>;
4134
4566
  ```
4135
4567
 
4136
4568
  **Parameters:**
4137
- - `args` — - Модель данных, единая схема (component/componentProps/validators) и (опционально) поведение
4569
+ - `args` — - Модель данных, layout-схема (component/componentProps) и (опционально) поведение
4138
4570
 
4139
4571
  **Returns:** Типизированная форма с Proxy-доступом к полям
4140
4572
 
4141
4573
  **Examples:**
4142
4574
 
4143
- Архитектура M1: `createModel` + единая схема + `createForm({ model, schema })`
4575
+ Архитектура M1: `createModel` + layout-схема + `createForm({ model, schema })`
4144
4576
  ```typescript
4145
- import { createModel, createForm, validateFormModel } from '@reformer/core';
4577
+ import { createModel, createForm } from '@reformer/core';
4578
+ import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
4146
4579
  import { required, email, minLength } from '@reformer/core/validators';
4147
4580
 
4148
4581
  interface UserForm {
@@ -4152,28 +4585,23 @@ password: string;
4152
4585
 
4153
4586
  const model = createModel<UserForm>({ email: '', password: '' });
4154
4587
 
4588
+ // Layout-схема НЕ несёт validators — только component/componentProps.
4155
4589
  const schema = {
4156
4590
  children: [
4157
- {
4158
- value: model.$.email,
4159
- component: Input,
4160
- validators: [required(), email()],
4161
- },
4162
- {
4163
- value: model.$.password,
4164
- component: Input,
4165
- validators: [required(), minLength(8)],
4166
- },
4591
+ { value: model.$.email, component: Input },
4592
+ { value: model.$.password, component: Input },
4167
4593
  ],
4168
4594
  };
4169
4595
 
4170
4596
  const form = createForm<UserForm>({ model, schema });
4171
-
4172
- // TypeScript знает о полях (нода привязана к сигналу модели):
4173
4597
  form.email.setValue('test@mail.com');
4174
4598
 
4175
- // Валидация всей модели по схеме (sync + async):
4176
- const { valid } = await validateFormModel(model, schema);
4599
+ // Валидация — ОТДЕЛЬНАЯ схема, прогон внешним раннером (ошибки сами доезжают до нод):
4600
+ const validation = defineValidationSchema<UserForm>(({ model }) => {
4601
+ validate(model.$.email, [required(), email()]);
4602
+ validate(model.$.password, [required(), minLength(8)]);
4603
+ });
4604
+ const ok = await validateModel(model, validation);
4177
4605
  ```
4178
4606
 
4179
4607
  _Source: src/form/create-form.ts_
@@ -4342,7 +4770,7 @@ _Source: src/form/behaviors-node.ts_
4342
4770
 
4343
4771
  **Signature:**
4344
4772
  ```typescript
4345
- export function email<TForm = unknown, TField extends string | undefined = string>(
4773
+ export function email<TForm = unknown, TField extends string | null | undefined = string>(
4346
4774
  options?: ValidateOptions
4347
4775
  ): Validator<TForm, TField>
4348
4776
  ```
@@ -4792,6 +5220,70 @@ export type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
4792
5220
 
4793
5221
  _Source: src/form/types/contracts.ts_
4794
5222
 
5223
+ ### FileLike
5224
+
5225
+ **Kind:** `interface`
5226
+
5227
+ Минимальный файлоподобный контракт, с которым работают file-валидаторы:
5228
+ нативный `File`, сериализуемый дескриптор загруженного файла и т.п.
5229
+
5230
+ **Signature:**
5231
+ ```typescript
5232
+ export interface FileLike {
5233
+ name: string;
5234
+ size?: number;
5235
+ type?: string;
5236
+ }
5237
+ ```
5238
+
5239
+ _Source: src/form/validation/validators/file-utils.ts_
5240
+
5241
+ ### fileType
5242
+
5243
+ **Kind:** `function`
5244
+
5245
+ Фабрика валидатора типа файла.
5246
+
5247
+ Проверяет каждый файл против accept-строки в синтаксисе нативного атрибута
5248
+ `<input type="file" accept>`: расширения (`.pdf`), точные MIME (`image/png`),
5249
+ wildcard-категории (`image/*`), список через запятую. Матчинг регистронезависимый,
5250
+ по `type` и/или расширению из `name` ({@link matchesFileAccept}).
5251
+
5252
+ Нужен потому, что нативный `accept` — только подсказка пикеру: на drag-and-drop
5253
+ и «All files» он не действует. Пустые значения (`null`/`undefined`/`''`/`[]`)
5254
+ пропускаются (используйте {@link required} для обязательности).
5255
+
5256
+ **Signature:**
5257
+ ```typescript
5258
+ export function fileType<TForm = unknown, TField = unknown>(
5259
+ accept: string,
5260
+ options?: ValidateOptions
5261
+ ): Validator<TForm, TField>
5262
+ ```
5263
+
5264
+ **Parameters:**
5265
+ - `accept` — - Accept-строка допустимых типов, например `'image/*,.pdf'`
5266
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
5267
+ попадают `accept` и `fileName` (первый нарушивший файл).
5268
+
5269
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
5270
+
5271
+ **Examples:**
5272
+
5273
+ Только изображения и PDF
5274
+ ```typescript
5275
+ import { fileType } from '@reformer/core/validators';
5276
+
5277
+ // Внутри FieldConfig схемы формы:
5278
+ documents: {
5279
+ value: model.$.documents,
5280
+ component: FileUploadField,
5281
+ validators: [fileType('image/*,.pdf', { message: 'Только изображения или PDF' })],
5282
+ },
5283
+ ```
5284
+
5285
+ _Source: src/form/validation/validators/file-type.ts_
5286
+
4795
5287
  ### FormArrayProxy
4796
5288
 
4797
5289
  **Kind:** `type`
@@ -5143,8 +5635,8 @@ _Source: src/form/types/deep-schema.ts_
5143
5635
 
5144
5636
  **Kind:** `interface`
5145
5637
 
5146
- Узел единой схемы M1 — дерево, обходимое `createForm({ model, schema })`,
5147
- `validateModel`/`validateFormModel` и рендерерами.
5638
+ Узел единой схемы M1 — layout-дерево, обходимое `createForm({ model, schema })`
5639
+ и рендерерами (schema-валидация живёт отдельно — `@reformer/core/validation`).
5148
5640
 
5149
5641
  Узел совмещает несколько ролей (различаются рантаймом по форме):
5150
5642
  - **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
@@ -5164,8 +5656,12 @@ export interface FormSchemaNode {
5164
5656
  * `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
5165
5657
  */
5166
5658
  value?: unknown;
5167
- /** UI-компонент. Опционален: core-часть работает без UI (значение/валидация). */
5168
- component?: ComponentType<any>;
5659
+ /**
5660
+ * UI-компонент либо нативный HTML-тег (`'div'`, `'p'`, `'h3'`) для презентационной вёрстки
5661
+ * прямо в схеме. Опционален: core-часть работает без UI (значение/валидация) и `component`
5662
+ * не интерпретирует — он доезжает до рендерера как есть.
5663
+ */
5664
+ component?: ElementType;
5169
5665
  /** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
5170
5666
  componentProps?: Record<string, unknown>;
5171
5667
  validators?: SchemaValidator[];
@@ -5301,7 +5797,7 @@ _Source: src/form/types/contracts.ts_
5301
5797
  ```typescript
5302
5798
  export function futureDate<
5303
5799
  TForm = unknown,
5304
- TField extends string | Date | undefined = string | Date,
5800
+ TField extends string | Date | null | undefined = string | Date,
5305
5801
  >(options?: ValidateOptions): Validator<TForm, TField>
5306
5802
  ```
5307
5803
 
@@ -5394,8 +5890,8 @@ _Source: src/form/type-guards.ts_
5394
5890
  GroupNode - узел для группы полей
5395
5891
 
5396
5892
  Создаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`
5397
- (M1: `createForm({ model, schema })`); валидация/behavior живут на слое модели
5398
- (`validateFormModel`, `computeFrom`/`enableWhen`/…), а не на ноде.
5893
+ (M1: `createForm({ model, schema })`); schema-валидация/behavior живут на слое модели
5894
+ (`validateModel` из `@reformer/core/validation`, `computeFrom`/`enableWhen`/…), а не на ноде.
5399
5895
 
5400
5896
  **Signature:**
5401
5897
  ```typescript
@@ -5519,9 +6015,10 @@ _Source: src/form/type-guards.ts_
5519
6015
 
5520
6016
  **Signature:**
5521
6017
  ```typescript
5522
- export function isDate<TForm = unknown, TField extends string | Date | undefined = string | Date>(
5523
- options?: ValidateOptions
5524
- ): Validator<TForm, TField>
6018
+ export function isDate<
6019
+ TForm = unknown,
6020
+ TField extends string | Date | null | undefined = string | Date,
6021
+ >(options?: ValidateOptions): Validator<TForm, TField>
5525
6022
  ```
5526
6023
 
5527
6024
  **Parameters:**
@@ -5610,6 +6107,25 @@ if (isFieldNode(node)) {
5610
6107
 
5611
6108
  _Source: src/form/type-guards.ts_
5612
6109
 
6110
+ ### isFileLike
6111
+
6112
+ **Kind:** `function`
6113
+
6114
+ Проверяет, что значение файлоподобно: объект со строковым `name` и опциональными
6115
+ числовым `size` / строковым `type`.
6116
+
6117
+ **Signature:**
6118
+ ```typescript
6119
+ export function isFileLike(value: unknown): value is FileLike
6120
+ ```
6121
+
6122
+ **Parameters:**
6123
+ - `value` — - Проверяемое значение
6124
+
6125
+ **Returns:** `true`, если значение соответствует {@link FileLike}
6126
+
6127
+ _Source: src/form/validation/validators/file-utils.ts_
6128
+
5613
6129
  ### isFormNode
5614
6130
 
5615
6131
  **Kind:** `function`
@@ -5742,6 +6258,30 @@ markDerived(model.$.total);
5742
6258
 
5743
6259
  _Source: src/state/derived-registry.ts_
5744
6260
 
6261
+ ### matchesFileAccept
6262
+
6263
+ **Kind:** `function`
6264
+
6265
+ Проверяет файл против accept-паттерна в синтаксисе нативного атрибута
6266
+ `<input type="file" accept>`: список через запятую из расширений (`.pdf`),
6267
+ точных MIME (`image/png`) и wildcard-категорий (`image/*`). Регистронезависимо.
6268
+
6269
+ Нужен собственный матчер, потому что `accept` у нативного input — только подсказка
6270
+ пикеру: на drag-and-drop и на программное значение он не действует.
6271
+
6272
+ **Signature:**
6273
+ ```typescript
6274
+ export function matchesFileAccept(file: Pick<FileLike, 'name' | 'type'>, accept: string): boolean
6275
+ ```
6276
+
6277
+ **Parameters:**
6278
+ - `file` — - Файлоподобное значение (`name` + опциональный `type`)
6279
+ - `accept` — - Accept-строка; пустая/пробельная строка матчит всё
6280
+
6281
+ **Returns:** `true`, если файл подходит хотя бы под один паттерн
6282
+
6283
+ _Source: src/form/validation/validators/file-utils.ts_
6284
+
5745
6285
  ### max
5746
6286
 
5747
6287
  **Kind:** `function`
@@ -5862,6 +6402,90 @@ validators: [maxDate(new Date(), { message: 'Дата не может быть
5862
6402
 
5863
6403
  _Source: src/form/validation/validators/max-date.ts_
5864
6404
 
6405
+ ### maxFiles
6406
+
6407
+ **Kind:** `function`
6408
+
6409
+ Фабрика валидатора максимального количества файлов.
6410
+
6411
+ Работает с массивом файлов (одиночный файл считается как 1). Пустые значения
6412
+ (`null`/`undefined`/`''`/`[]`) пропускаются (используйте {@link required}
6413
+ для обязательности).
6414
+
6415
+ **Signature:**
6416
+ ```typescript
6417
+ export function maxFiles<TForm = unknown, TField = unknown>(
6418
+ max: number,
6419
+ options?: ValidateOptions
6420
+ ): Validator<TForm, TField>
6421
+ ```
6422
+
6423
+ **Parameters:**
6424
+ - `max` — - Максимально допустимое количество файлов (включительно)
6425
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
6426
+ попадают `maxFiles` и `actualCount`.
6427
+
6428
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
6429
+
6430
+ **Examples:**
6431
+
6432
+ Не более трёх вложений
6433
+ ```typescript
6434
+ import { maxFiles } from '@reformer/core/validators';
6435
+
6436
+ // Внутри FieldConfig схемы формы:
6437
+ documents: {
6438
+ value: model.$.documents,
6439
+ component: FileUploadField,
6440
+ validators: [maxFiles(3, { message: 'Максимум 3 файла' })],
6441
+ },
6442
+ ```
6443
+
6444
+ _Source: src/form/validation/validators/max-files.ts_
6445
+
6446
+ ### maxFileSize
6447
+
6448
+ **Kind:** `function`
6449
+
6450
+ Фабрика валидатора максимального размера файла.
6451
+
6452
+ Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
6453
+ `name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
6454
+ Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
6455
+ (например, дескриптор загруженного файла без размера) пропускаются
6456
+ (используйте {@link required} для обязательности).
6457
+
6458
+ **Signature:**
6459
+ ```typescript
6460
+ export function maxFileSize<TForm = unknown, TField = unknown>(
6461
+ maxSize: number,
6462
+ options?: ValidateOptions
6463
+ ): Validator<TForm, TField>
6464
+ ```
6465
+
6466
+ **Parameters:**
6467
+ - `maxSize` — - Максимально допустимый размер файла в байтах (включительно)
6468
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
6469
+ попадают `maxFileSize`, `fileName` и `actualSize`.
6470
+
6471
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
6472
+
6473
+ **Examples:**
6474
+
6475
+ Ограничение размера вложений
6476
+ ```typescript
6477
+ import { maxFileSize } from '@reformer/core/validators';
6478
+
6479
+ // Внутри FieldConfig схемы формы:
6480
+ documents: {
6481
+ value: model.$.documents,
6482
+ component: FileUploadField,
6483
+ validators: [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })],
6484
+ },
6485
+ ```
6486
+
6487
+ _Source: src/form/validation/validators/max-file-size.ts_
6488
+
5865
6489
  ### maxLength
5866
6490
 
5867
6491
  **Kind:** `function`
@@ -5904,6 +6528,47 @@ validators: [maxLength(500, { message: 'Максимум 500 символов' }
5904
6528
 
5905
6529
  _Source: src/form/validation/validators/max-length.ts_
5906
6530
 
6531
+ ### maxTotalFileSize
6532
+
6533
+ **Kind:** `function`
6534
+
6535
+ Фабрика валидатора суммарного размера всех файлов.
6536
+
6537
+ Складывает `size` всех файлов значения (элементы без числового `size` в сумму
6538
+ не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
6539
+ (используйте {@link required} для обязательности).
6540
+
6541
+ **Signature:**
6542
+ ```typescript
6543
+ export function maxTotalFileSize<TForm = unknown, TField = unknown>(
6544
+ maxTotal: number,
6545
+ options?: ValidateOptions
6546
+ ): Validator<TForm, TField>
6547
+ ```
6548
+
6549
+ **Parameters:**
6550
+ - `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
6551
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
6552
+ попадают `maxTotalFileSize` и `actualTotal`.
6553
+
6554
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
6555
+
6556
+ **Examples:**
6557
+
6558
+ Суммарно не более 20 МБ
6559
+ ```typescript
6560
+ import { maxTotalFileSize } from '@reformer/core/validators';
6561
+
6562
+ // Внутри FieldConfig схемы формы:
6563
+ documents: {
6564
+ value: model.$.documents,
6565
+ component: FileUploadField,
6566
+ validators: [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })],
6567
+ },
6568
+ ```
6569
+
6570
+ _Source: src/form/validation/validators/max-total-file-size.ts_
6571
+
5907
6572
  ### min
5908
6573
 
5909
6574
  **Kind:** `function`
@@ -6024,6 +6689,90 @@ validators: [required(), minDate(new Date(), { message: 'Дата не рань
6024
6689
 
6025
6690
  _Source: src/form/validation/validators/min-date.ts_
6026
6691
 
6692
+ ### minFiles
6693
+
6694
+ **Kind:** `function`
6695
+
6696
+ Фабрика валидатора минимального количества файлов.
6697
+
6698
+ Работает с массивом файлов (одиночный файл считается как 1). Пустые значения
6699
+ (`null`/`undefined`/`''`/`[]`) пропускаются — «нужен хотя бы один файл» выражается
6700
+ связкой `required()` + `minFiles(n)`, как и у {@link minLength}.
6701
+
6702
+ **Signature:**
6703
+ ```typescript
6704
+ export function minFiles<TForm = unknown, TField = unknown>(
6705
+ min: number,
6706
+ options?: ValidateOptions
6707
+ ): Validator<TForm, TField>
6708
+ ```
6709
+
6710
+ **Parameters:**
6711
+ - `min` — - Минимально допустимое количество файлов (включительно)
6712
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
6713
+ попадают `minFiles` и `actualCount`.
6714
+
6715
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
6716
+
6717
+ **Examples:**
6718
+
6719
+ Не менее двух документов
6720
+ ```typescript
6721
+ import { required, minFiles } from '@reformer/core/validators';
6722
+
6723
+ // Внутри FieldConfig схемы формы:
6724
+ documents: {
6725
+ value: model.$.documents,
6726
+ component: FileUploadField,
6727
+ validators: [required(), minFiles(2, { message: 'Приложите минимум 2 документа' })],
6728
+ },
6729
+ ```
6730
+
6731
+ _Source: src/form/validation/validators/min-files.ts_
6732
+
6733
+ ### minFileSize
6734
+
6735
+ **Kind:** `function`
6736
+
6737
+ Фабрика валидатора минимального размера файла.
6738
+
6739
+ Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
6740
+ `name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
6741
+ Типовой сценарий — отсев пустых (0-байтовых) файлов: `minFileSize(1)`.
6742
+ Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
6743
+ пропускаются (используйте {@link required} для обязательности).
6744
+
6745
+ **Signature:**
6746
+ ```typescript
6747
+ export function minFileSize<TForm = unknown, TField = unknown>(
6748
+ minSize: number,
6749
+ options?: ValidateOptions
6750
+ ): Validator<TForm, TField>
6751
+ ```
6752
+
6753
+ **Parameters:**
6754
+ - `minSize` — - Минимально допустимый размер файла в байтах (включительно)
6755
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
6756
+ попадают `minFileSize`, `fileName` и `actualSize`.
6757
+
6758
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
6759
+
6760
+ **Examples:**
6761
+
6762
+ Отсев пустых файлов
6763
+ ```typescript
6764
+ import { minFileSize } from '@reformer/core/validators';
6765
+
6766
+ // Внутри FieldConfig схемы формы:
6767
+ documents: {
6768
+ value: model.$.documents,
6769
+ component: FileUploadField,
6770
+ validators: [minFileSize(1, { message: 'Файл пустой' })],
6771
+ },
6772
+ ```
6773
+
6774
+ _Source: src/form/validation/validators/min-file-size.ts_
6775
+
6027
6776
  ### minLength
6028
6777
 
6029
6778
  **Kind:** `function`
@@ -6256,44 +7005,6 @@ export type ModelSignals<T> = {
6256
7005
 
6257
7006
  _Source: src/state/types.ts_
6258
7007
 
6259
- ### ModelValidationResult
6260
-
6261
- **Kind:** `interface`
6262
-
6263
- Результат валидации: ошибки по пути поля.
6264
-
6265
- **Signature:**
6266
- ```typescript
6267
- export interface ModelValidationResult {
6268
- valid: boolean;
6269
- /** Ключ — путь поля (`'coBorrowers.0.relationship'`). Только поля с ошибками. */
6270
- errors: Record<string, ValidationError[]>;
6271
- }
6272
- ```
6273
-
6274
- _Source: src/form/validate-model-core.ts_
6275
-
6276
- ### ModelValidator
6277
-
6278
- **Kind:** `type`
6279
-
6280
- Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
6281
- массива или корень); `root` — корневая модель.
6282
-
6283
- Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
6284
- `FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
6285
-
6286
- **Signature:**
6287
- ```typescript
6288
- export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
6289
- value: TValue,
6290
- model: TModel,
6291
- root: TRoot
6292
- ) => ValidationError | null | Promise<ValidationError | null>;
6293
- ```
6294
-
6295
- _Source: src/form/validate-model-core.ts_
6296
-
6297
7008
  ### ModelValue
6298
7009
 
6299
7010
  **Kind:** `type`
@@ -6511,9 +7222,10 @@ _Source: src/form/validation/validators/non-zero.ts_
6511
7222
 
6512
7223
  **Signature:**
6513
7224
  ```typescript
6514
- export function pastDate<TForm = unknown, TField extends string | Date | undefined = string | Date>(
6515
- options?: ValidateOptions
6516
- ): Validator<TForm, TField>
7225
+ export function pastDate<
7226
+ TForm = unknown,
7227
+ TField extends string | Date | null | undefined = string | Date,
7228
+ >(options?: ValidateOptions): Validator<TForm, TField>
6517
7229
  ```
6518
7230
 
6519
7231
  **Parameters:**
@@ -6565,7 +7277,7 @@ _Source: src/state/types.ts_
6565
7277
 
6566
7278
  **Signature:**
6567
7279
  ```typescript
6568
- export function pattern<TForm = unknown, TField extends string | undefined = string>(
7280
+ export function pattern<TForm = unknown, TField extends string | null | undefined = string>(
6569
7281
  regex: RegExp,
6570
7282
  options?: ValidateOptions
6571
7283
  ): Validator<TForm, TField>
@@ -6613,7 +7325,7 @@ _Source: src/form/validation/validators/pattern.ts_
6613
7325
 
6614
7326
  **Signature:**
6615
7327
  ```typescript
6616
- export function phone<TForm = unknown, TField extends string | undefined = string>(
7328
+ export function phone<TForm = unknown, TField extends string | null | undefined = string>(
6617
7329
  options?: PhoneValidatorOptions
6618
7330
  ): Validator<TForm, TField>
6619
7331
  ```
@@ -6778,8 +7490,8 @@ _Source: src/form/types/index.ts_
6778
7490
 
6779
7491
  **Kind:** `function`
6780
7492
 
6781
- Вызывает `revalidate()` при изменении зависимостей (не на инициализации). Под M1 валидация
6782
- on-demand (`validateFormModel`), поэтому ревалидация выражается явным колбэком.
7493
+ Вызывает `revalidate()` при изменении зависимостей (не на инициализации). Валидация on-demand
7494
+ (`validateModel` из `@reformer/core/validation`), поэтому ревалидация выражается явным колбэком.
6783
7495
 
6784
7496
  **Signature:**
6785
7497
  ```typescript
@@ -6792,7 +7504,7 @@ export function revalidateWhen(
6792
7504
  **Examples:**
6793
7505
 
6794
7506
  ```typescript
6795
- revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
7507
+ revalidateWhen([model.$.maxAmount], () => void validateModel(model, schema));
6796
7508
  ```
6797
7509
 
6798
7510
  _Source: src/state/behaviors-value.ts_
@@ -6935,10 +7647,10 @@ _Source: src/form/types/schema-node.ts_
6935
7647
 
6936
7648
  **Kind:** `type`
6937
7649
 
6938
- Валидатор поля схемы. Узел хранит validators гетерогенно: движок `validate*` вызывает их как
6939
- `(value, scope, root)`, но сюда кладут любой контракт — `ValidatorFn` (value),
6940
- `Validator` (value, control, root) или `ModelValidator` (value, model, root). Поэтому тип
6941
- намеренно широкий по параметрам.
7650
+ Валидатор поля схемы (legacy). Дерево-движок, вызывавший `validators` узла как
7651
+ `(value, scope, root)`, удалён — живые правила это `Rule<T>` в `validate(sig, [...])`
7652
+ (`@reformer/core/validation`). Тип остаётся широким по параметрам для совместимости
7653
+ узлов, всё ещё несущих `validators` (гетерогенные контракты `ValidatorFn`/`Validator`).
6942
7654
 
6943
7655
  **Signature:**
6944
7656
  ```typescript
@@ -7113,6 +7825,27 @@ syncFields(model.$.field1, model.$.field2);
7113
7825
 
7114
7826
  _Source: src/state/behaviors-value.ts_
7115
7827
 
7828
+ ### toFileArray
7829
+
7830
+ **Kind:** `function`
7831
+
7832
+ Нормализует значение file-поля в массив файлоподобных элементов.
7833
+
7834
+ Одиночный {@link FileLike} оборачивается в массив; из массива отбираются только
7835
+ файлоподобные элементы. Для прочих значений — `null` (валидатор пропускает).
7836
+
7837
+ **Signature:**
7838
+ ```typescript
7839
+ export function toFileArray(value: unknown): FileLike[] | null
7840
+ ```
7841
+
7842
+ **Parameters:**
7843
+ - `value` — - Значение поля (`FileLike`, `FileLike[]` или что-то ещё)
7844
+
7845
+ **Returns:** Массив файлоподобных элементов или `null`
7846
+
7847
+ _Source: src/form/validation/validators/file-utils.ts_
7848
+
7116
7849
  ### transformValue
7117
7850
 
7118
7851
  **Kind:** `function`
@@ -7253,7 +7986,7 @@ _Source: src/state/derived-registry.ts_
7253
7986
 
7254
7987
  **Signature:**
7255
7988
  ```typescript
7256
- export function url<TForm = unknown, TField extends string | undefined = string>(
7989
+ export function url<TForm = unknown, TField extends string | null | undefined = string>(
7257
7990
  options?: UrlValidatorOptions
7258
7991
  ): Validator<TForm, TField>
7259
7992
  ```
@@ -7756,94 +8489,6 @@ export interface ValidateAsyncOptions extends ValidateOptions {
7756
8489
 
7757
8490
  _Source: src/form/types/validation-schema.ts_
7758
8491
 
7759
- ### validateFormModel
7760
-
7761
- **Kind:** `function`
7762
-
7763
- In-form валидация: прогоняет {@link validateModel} и роутит ошибки в ноды формы
7764
- (через реестр сигнал→нода): `node.setErrors(errors[path] ?? [])` — заодно очищает прошедшие поля
7765
- И поля выключенных веток (`{ when, children }` с ложным условием). Дерево обходится ОДИН раз.
7766
-
7767
- **Signature:**
7768
- ```typescript
7769
- export async function validateFormModel<T>(
7770
- model: FormModel<T>,
7771
- schema: FormSchemaNode
7772
- ): Promise<ModelValidationResult>
7773
- ```
7774
-
7775
- **Parameters:**
7776
- - `model` — - Модель данных ({@link FormModel}) — источник значений полей.
7777
- - `schema` — - Единая схема формы (та же, что передавалась в `createForm`).
7778
-
7779
- **Returns:**
7780
-
7781
- **Examples:**
7782
-
7783
- Валидация перед submit (ошибки показываются в UI автоматически)
7784
- ```typescript
7785
- const form = createForm({ model, schema });
7786
- form.touchAll();
7787
- const res = await validateFormModel(model, schema);
7788
- if (res.valid) await api.save(model.get());
7789
- ```
7790
-
7791
- _Source: src/form/validate-model.ts_
7792
-
7793
- ### validateModel
7794
-
7795
- **Kind:** `function`
7796
-
7797
- Headless-валидация данных (sync + async). Работает без UI/нод.
7798
-
7799
- **Signature:**
7800
- ```typescript
7801
- export async function validateModel<T>(
7802
- model: FormModel<T>,
7803
- schema: FormSchemaNode
7804
- ): Promise<ModelValidationResult>
7805
- ```
7806
-
7807
- **Examples:**
7808
-
7809
- ```typescript
7810
- const res = await validateModel(model, schema);
7811
- if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
7812
- ```
7813
-
7814
- _Source: src/form/validate-model-core.ts_
7815
-
7816
- ### validateModelSync
7817
-
7818
- **Kind:** `function`
7819
-
7820
- Синхронная headless-валидация данных. Асинхронные валидаторы пропускаются.
7821
-
7822
- **Signature:**
7823
- ```typescript
7824
- export function validateModelSync<T>(
7825
- model: FormModel<T>,
7826
- schema: FormSchemaNode
7827
- ): ModelValidationResult
7828
- ```
7829
-
7830
- **Parameters:**
7831
- - `model` — - Модель данных ({@link FormModel}), из сигналов которой читаются значения полей.
7832
- - `schema` — - Единая схема формы (дерево узлов с `value`/`validators`, `{ when, children }`,
7833
- секциями массивов). Обходится один раз для сбора активных полей.
7834
-
7835
- **Returns:**
7836
-
7837
- **Examples:**
7838
-
7839
- Быстрая синхронная проверка (например, для gate «можно ли перейти на след. шаг»)
7840
- ```typescript
7841
- const res = validateModelSync(model, schema);
7842
- if (!res.valid) console.log(res.errors); // { 'email': [{ code, message }], ... }
7843
- ```
7844
-
7845
- _Source: src/form/validate-model-core.ts_
7846
-
7847
8492
  ### ValidateOptions
7848
8493
 
7849
8494
  **Kind:** `interface`
@@ -7887,20 +8532,7 @@ _Source: src/form/types/contracts.ts_
7887
8532
 
7888
8533
  **Kind:** `type`
7889
8534
 
7890
- Чистый синхронный валидатор поля.
7891
-
7892
- Сигнатура зеркалит то, что реально вызывает движок M1
7893
- (`validateModel`/`validateFormModel`/`validateModelSync`): `validator(value, scope, root)`.
7894
- - `value` — значение поля (`TField`);
7895
- - `scope` — ближайшая scope-**модель** (под-модель элемента массива или корень). Из `TForm`/`TField`
7896
- её тип не выводится, поэтому `unknown` — потребитель сужает сам (для типизированного scope
7897
- используйте {@link ModelValidator}`<TField, TScope, TForm>`);
7898
- - `root` — корневая модель формы `FormModel<TForm>` (реактивный value-proxy: `root.field` читает
7899
- значение). Возвращает `ValidationError` либо `null`. Не знает про реестр валидации.
7900
-
7901
- Совместим с полем `validators` узла схемы (см. `SchemaValidator`) — там же лежат `ModelValidator`
7902
- и `ValidatorFn`. Встроенные фабрики (`required()`/`email()`/…) возвращают `(value) => …` и
7903
- дополнительные аргументы игнорируют.
8535
+ Чистый синхронный валидатор поля (legacy-сигнатура `(value, scope, root)`).
7904
8536
 
7905
8537
  **Signature:**
7906
8538
  ```typescript
@@ -7911,23 +8543,12 @@ export type Validator<TForm, TField> = (
7911
8543
  ) => ValidationError | null;
7912
8544
  ```
7913
8545
 
7914
- **Examples:**
7915
-
7916
- Кастомный валидатор в массиве `validators` поля схемы
7917
- ```typescript
7918
- const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
7919
- if (value < 18) return { code: 'tooYoung', message: '18+' };
7920
- // root — FormModel<MyForm>: root.someOtherField читается как значение
7921
- return null;
7922
- };
7923
-
7924
- // Фабрики и кастомные валидаторы кладутся в `validators: [...]` поля:
7925
- const schema = {
7926
- children: [
7927
- { value: model.$.age, component: Input, validators: [required(), isAdult] },
7928
- ],
7929
- };
7930
- ```
8546
+ **Deprecated:** Осиротевший остаток удалённого дерево-движка (`validateFormModel`). Живой контракт —
8547
+ `Rule<T> = (value) => ValidationError | null` из `@reformer/core/validation`: правила
8548
+ передаются в `validate(sig, [rules])` внутри `defineValidationSchema` и запускаются
8549
+ `validateModel(model, schema)`. Cross-field — оператор `cross(sig, (f) => …)` над снапшотом
8550
+ `model.get()` (третий аргумент `root` больше не нужен). Тип экспортируется только ради
8551
+ обратной совместимости (`SchemaValidator`-union).
7931
8552
 
7932
8553
  _Source: src/form/types/validation-schema.ts_
7933
8554