@reformer/core 8.0.0 → 9.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 (52) hide show
  1. package/README.md +52 -57
  2. package/dist/behaviors-Br4Im38V.js +263 -0
  3. package/dist/behaviors.js +146 -146
  4. package/dist/core/factories/node-factory.d.ts +5 -4
  5. package/dist/core/model/behaviors.d.ts +15 -0
  6. package/dist/core/model/types.d.ts +41 -12
  7. package/dist/core/model/validate-model.d.ts +8 -4
  8. package/dist/core/nodes/field-node.d.ts +2 -2
  9. package/dist/core/nodes/group-node.d.ts +39 -3
  10. package/dist/core/nodes/model-array-node.d.ts +13 -1
  11. package/dist/core/types/deep-schema.d.ts +39 -5
  12. package/dist/core/types/form-proxy.d.ts +27 -2
  13. package/dist/core/types/index.d.ts +4 -10
  14. package/dist/core/types/schema-node.d.ts +79 -0
  15. package/dist/core/types/validation-schema.d.ts +37 -16
  16. package/dist/core/utils/create-form.d.ts +4 -3
  17. package/dist/core/utils/derived-registry.d.ts +29 -0
  18. package/dist/core/utils/error-handler.d.ts +4 -1
  19. package/dist/core/utils/form-submitter.d.ts +8 -4
  20. package/dist/core/utils/index.d.ts +1 -1
  21. package/dist/core/utils/safe-effect.d.ts +27 -7
  22. package/dist/core/utils/type-guards.d.ts +3 -3
  23. package/dist/core/validation/validators/date-utils.d.ts +5 -1
  24. package/dist/core/validation/validators/integer.d.ts +1 -1
  25. package/dist/core/validation/validators/is-number.d.ts +1 -1
  26. package/dist/core/validation/validators/max.d.ts +1 -1
  27. package/dist/core/validation/validators/min.d.ts +1 -1
  28. package/dist/core/validation/validators/multiple-of.d.ts +1 -1
  29. package/dist/core/validation/validators/non-negative.d.ts +1 -1
  30. package/dist/core/validation/validators/non-zero.d.ts +1 -1
  31. package/dist/core/validation/validators/required.d.ts +2 -1
  32. package/dist/{date-utils-xUWFslTj.js → date-utils-dTT_x_Be.js} +12 -10
  33. package/dist/hooks/types.d.ts +31 -1
  34. package/dist/hooks/useArrayLength.d.ts +1 -2
  35. package/dist/hooks/useFormControl.d.ts +2 -2
  36. package/dist/hooks/useSignalSubscription.d.ts +8 -1
  37. package/dist/index.js +625 -617
  38. package/dist/signals.d.ts +13 -0
  39. package/dist/signals.js +9 -0
  40. package/dist/validators/future-date.js +1 -1
  41. package/dist/validators/is-date.js +1 -1
  42. package/dist/validators/max-age.js +1 -1
  43. package/dist/validators/max-date.js +1 -1
  44. package/dist/validators/min-age.js +1 -1
  45. package/dist/validators/min-date.js +1 -1
  46. package/dist/validators/multiple-of.js +11 -7
  47. package/dist/validators/past-date.js +1 -1
  48. package/dist/validators/pattern.js +7 -6
  49. package/dist/validators/required.js +5 -9
  50. package/llms.txt +715 -175
  51. package/package.json +6 -2
  52. package/dist/behaviors-O9a1Djj9.js +0 -147
package/llms.txt CHANGED
@@ -22,6 +22,7 @@
22
22
  - 15-project-structure.md — project-structure
23
23
  - 16-ui-components.md — ui-components
24
24
  - 17-nonexistent-api.md — nonexistent-api
25
+ - 18-conditional-fields.md — Условные поля — видимость, доступность и валидация
25
26
  - 19-reading-values.md — reading-values
26
27
  - 20-compute-vs-watch.md — compute-vs-watch
27
28
  - 21-array-operations.md — array-operations
@@ -1040,6 +1041,39 @@ const form = createForm<MyForm>({ model, schema });
1040
1041
  > он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
1041
1042
  > См. `30-type-safety-recipes.md`.
1042
1043
 
1044
+ ### Один массив — три слоя (три разных движка)
1045
+
1046
+ Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
1047
+ перепутать, но каждая корректна только в своём контексте:
1048
+
1049
+ 1. **Layout-схема `createForm`** — единственная форма, которую ест `createForm`:
1050
+ `{ array: model.<path>, item: (itemModel) => subSchema }`. Массив связывается через
1051
+ **value-proxy** `model.properties` (он несёт `__path`), а **не** через сигнальный
1052
+ `model.$.properties`.
1053
+
1054
+ ```typescript
1055
+ // узел схемы для createForm({ model, schema })
1056
+ properties: { array: model.properties, item: propertyItem },
1057
+ ```
1058
+
1059
+ 2. **Validation-схема `validateFormModel`** — секция массива описывается через `componentProps`:
1060
+ `{ componentProps: { control: model.<array>, itemComponent: (item) => subSchema } }`. Движок
1061
+ узнаёт секцию по `componentProps.itemComponent` + `control` и обходит её per-item.
1062
+
1063
+ ```typescript
1064
+ // узел схемы для validateFormModel(model, schema)
1065
+ { componentProps: { control: model.properties, itemComponent: propertyRules } }
1066
+ ```
1067
+
1068
+ 3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`ModelArrayNode`),
1069
+ а не со схемой: `<FormArray.Root control={form.properties}>` (CDK) или `FormArraySection` из
1070
+ `@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).
1071
+
1072
+ > **Не путай форму по движку.** `createForm` принимает **только** `{ array, item }`;
1073
+ > `componentProps.{ control, itemComponent }` — это форма validation-схемы; `FormArray.Root
1074
+ > control={form.x}` (или `FormArraySection`) — рендер. `control`/`itemComponent` в layout-схеме
1075
+ > `createForm` не подхватятся, а `{ array, item }` схема валидации не обходит.
1076
+
1043
1077
  ### Array operations — на модели
1044
1078
 
1045
1079
  Мутации массива делаются через `ModelArray` (`model.items`), а не через ноду формы:
@@ -1060,6 +1094,16 @@ model.items.map((item, i) => item.name); // item — FormModel<Item>
1060
1094
  > (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
1061
1095
  > берутся из `item`-фабрики схемы автоматически.
1062
1096
 
1097
+ > **Очистка массива в behavior.** Тот же `model.<array>.clear()` (см. список операций выше) —
1098
+ > способ очистить коллекцию **вне React**, из behavior: он мутирует модель напрямую, минуя ноды
1099
+ > формы. Типичный случай — сбросить массив при выключении флага:
1100
+ >
1101
+ > ```typescript
1102
+ > onChange(model.$.hasProperty, (on) => {
1103
+ > if (!on) model.properties.clear();
1104
+ > });
1105
+ > ```
1106
+
1063
1107
  ### Rendering Arrays
1064
1108
 
1065
1109
  Каждый элемент массива — под-форма (`FormProxy<Item>`). Итерируй через `form.items.map`:
@@ -1112,7 +1156,12 @@ const uniqueNames: ModelValidator<unknown, unknown, MyForm> = (_value, _scope, r
1112
1156
  };
1113
1157
  ```
1114
1158
 
1115
- ## 11. 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)
1159
+ ## 11. See also
1160
+
1161
+ - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, per-item обход `validateFormModel`
1162
+ - CDK / ui-kit form-array (`FormArray.Root`, `FormArraySection`) — `find_recipe(topic="form-array")`
1163
+
1164
+ ## 12. 10. ASYNC REACTION — onChange (CRITICALLY IMPORTANT)
1116
1165
 
1117
1166
  Для async-реакции на изменение поля (загрузка зависимых опций, справочников) используй
1118
1167
  `onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно
@@ -1166,7 +1215,7 @@ const stop = watchField(model.$.country, (country) => {
1166
1215
  // stop() — отписаться
1167
1216
  ```
1168
1217
 
1169
- ## 12. 11. ARRAY CLEANUP PATTERN
1218
+ ## 13. 11. ARRAY CLEANUP PATTERN
1170
1219
 
1171
1220
  Очистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на
1172
1221
  модели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.
@@ -1198,7 +1247,7 @@ function clearWhenOff(flag: ReadonlySignal<boolean>, array: { clear(): void }):
1198
1247
  clearWhenOff(model.$.hasProperty, model.properties);
1199
1248
  ```
1200
1249
 
1201
- ## 13. 12. MULTI-STEP FORM VALIDATION
1250
+ ## 14. 12. MULTI-STEP FORM VALIDATION
1202
1251
 
1203
1252
  Каждый шаг — своя под-схема валидации (дерево узлов `{ children: [...] }`). Переход к
1204
1253
  следующему шагу проверяется `validateFormModel(model, stepSchema)`; полный submit — по
@@ -1275,7 +1324,7 @@ function MultiStepForm() {
1275
1324
  }
1276
1325
  ```
1277
1326
 
1278
- ## 14. 13. EXTENDED COMMON MISTAKES
1327
+ ## 15. 13. EXTENDED COMMON MISTAKES
1279
1328
 
1280
1329
  ### Reuse via apply / applyEach — не дублируй код
1281
1330
 
@@ -1357,30 +1406,37 @@ revalidateWhen([model.$.field2], () => validateFormModel(model, schema));
1357
1406
  > `validateGroup`/`validateTree`/`validateForm`, типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn`
1358
1407
  > и `ctx.form.*`/`ctx.setFieldValue` — из старой path-based архитектуры и УДАЛЕНЫ. См. `17-nonexistent-api.md`.
1359
1408
 
1360
- ## 15. 14. PROJECT STRUCTURE (COLOCATION)
1409
+ ## 16. 14. PROJECT STRUCTURE (COLOCATION)
1410
+
1411
+ One form is one module folder. **Default layout — flat minimalist:** every concern is a single
1412
+ file at the module root, and the whole form (entry + all wizard steps inline) lives in one
1413
+ `index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
1414
+ carried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a
1415
+ render-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add
1416
+ `renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`
1417
+ nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
1418
+ and rendered with `FormArraySection` — no per-step component files.
1361
1419
 
1362
1420
  ```
1363
1421
  src/
1364
- ├── components/ui/ # Reusable UI components
1365
- │ ├── FormField.tsx
1366
- │ └── FormArraySection.tsx
1422
+ ├── components/ui/ # App-wide reusable UI (FormField, FormArraySection, ...)
1367
1423
 
1368
1424
  ├── forms/
1369
- │ └── [form-name]/ # Form module
1370
- │ ├── types.ts # Form data type (type-alias)
1371
- │ ├── model.ts # createModel factory + initial values
1372
- │ ├── schema.ts # createForm schema (value: model.$.x)
1373
- │ ├── validation.ts # validation schema + validateFormModel config
1374
- │ ├── behavior.ts # defineFormBehavior(...)
1375
- │ ├── [FormName]Form.tsx # Main component
1376
-
1377
- ├── steps/ # Multi-step wizard
1378
- │ │ └── loan-info/
1379
- │ │ └── LoanInfoForm.tsx
1380
- │ │
1381
- │ └── nested-forms/ # Reusable sub-forms (Address, PersonalData, ...)
1425
+ │ └── [form-name]/ # Form module — flat, one file per concern
1426
+ │ ├── index.tsx # entry + whole form: createModel→createForm→<FormWizard> with all steps inline; arrays via FormArraySection
1427
+ │ ├── types.ts # form type + enums + { value, label } option type + constant dictionaries
1428
+ │ ├── model.ts # createModel + initial values + empty-array-element factories
1429
+ │ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
1430
+ │ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
1431
+ │ ├── validation.ts # ALL validation → { validateStep, validateAll }
1432
+ ├── data-sources.ts # options + async loaders (dataSources)
1433
+ └── api.ts # submit + prefill/load
1382
1434
  ```
1383
1435
 
1436
+ Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
1437
+ `index.tsx`; validation is a single `validation.ts`.** This is the working default for
1438
+ almost every form — reach for folders only when a file stops fitting on a screen.
1439
+
1384
1440
  ### Key Files
1385
1441
 
1386
1442
  ```typescript
@@ -1396,7 +1452,7 @@ import { createModel, type FormModel } from '@reformer/core';
1396
1452
  export const createCreditApplicationModel = (): FormModel<CreditApplicationForm> =>
1397
1453
  createModel<CreditApplicationForm>(createInitialCreditApplication());
1398
1454
 
1399
- // forms/credit-application/schema.ts
1455
+ // forms/credit-application/form.schema.ts
1400
1456
  import type { FormModel } from '@reformer/core';
1401
1457
  export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
1402
1458
  loanType: { value: model.$.loanType, component: Select, componentProps: { /* ... */ } },
@@ -1404,14 +1460,14 @@ export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>)
1404
1460
  properties: { array: model.properties, item: propertyItem },
1405
1461
  });
1406
1462
 
1407
- // forms/credit-application/behavior.ts
1463
+ // forms/credit-application/form.behavior.ts
1408
1464
  import { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';
1409
1465
  export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(({ model }) => {
1410
1466
  compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
1411
1467
  enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
1412
1468
  });
1413
1469
 
1414
- // forms/credit-application/create-form.tsсборка
1470
+ // forms/credit-application/index.tsxentry: assembles the form, renders <FormWizard> with all steps inline
1415
1471
  import { createForm } from '@reformer/core';
1416
1472
  export const createCreditApplicationForm = () => {
1417
1473
  const model = createCreditApplicationModel();
@@ -1419,15 +1475,64 @@ export const createCreditApplicationForm = () => {
1419
1475
  };
1420
1476
  ```
1421
1477
 
1478
+ ### Scaling up: folders (large forms)
1479
+
1480
+ When the flat module gets unwieldy — steps you want in their own files, sub-forms reused across
1481
+ steps, calc/validators that outgrow one file — promote it to a three-folder module: `lib/`
1482
+ (domain raw material), `schema/` (the form definition), `components/` (React layout), plus the
1483
+ entry component and `index.ts`. Keep the module root to just the entry + `index.ts`; everything
1484
+ else lives in a folder.
1485
+
1486
+ ```
1487
+ forms/
1488
+ └── [form-name]/ # Form module — folders (scale-up)
1489
+ ├── [FormName]Form.tsx # Entry: builds model+form, renders FormWizard + step components
1490
+ ├── index.ts # Public re-exports of the module
1491
+
1492
+ ├── lib/ # Domain helpers (target-agnostic)
1493
+ │ ├── types.ts # Form interface + field enums + { value, label } option type
1494
+ │ ├── constants.ts # Option dictionaries (LOAN_TYPES, GENDERS, ...)
1495
+ │ ├── calc.ts # Pure fns for derived fields (age, monthlyPayment, ...)
1496
+ │ ├── custom-validators.ts # Reusable validator factories
1497
+ │ └── api.ts # Data sources + submit
1498
+
1499
+ ├── schema/ # The form definition
1500
+ │ ├── model.ts # createModel factory + initial values + array-element factories
1501
+ │ ├── schema.ts # createForm schema tree ({ value: model.$.x, component })
1502
+ │ ├── validation.ts # validation schema + validateFormModel config
1503
+ │ ├── behavior.ts # defineFormBehavior(...)
1504
+ │ └── create-form.ts # Assembly: createForm({ model, schema, behavior })
1505
+
1506
+ └── components/
1507
+ ├── steps/ # One component per wizard step
1508
+ ├── nested-forms/ # Reusable sub-forms (Address, PersonalData, ...)
1509
+ └── ui/ # Form-specific helper blocks (summary, warnings, sections)
1510
+ ```
1511
+
1512
+ Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
1513
+ `schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
1514
+ may co-locate each step's `schema.ts` / `validation.ts` / `behavior.ts` inside its
1515
+ `steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
1516
+ the guide below.
1517
+
1422
1518
  ### Scaling
1423
1519
 
1424
- | Complexity | Structure |
1425
- | ---------- | ---------------------------------------------------------------------------- |
1426
- | Simple | Single file: `ContactForm.tsx` (model + schema + component) |
1427
- | Medium | Separate files: `types.ts`, `model.ts`, `schema.ts`, `validation.ts`, `Form.tsx` |
1428
- | Complex | Full colocation with `steps/`, `nested-forms/`, `behavior.ts` |
1520
+ | Complexity | Structure |
1521
+ | ---------- | --------------------------------------------------------------------------------------- |
1522
+ | Simple | Single file: `index.tsx` (model + schema + behavior + component) |
1523
+ | **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |
1524
+ | Folders (`lib/` + `schema/` + `components/`) | Large forms: split concerns into folders, one component per step, reusable `nested-forms/` |
1525
+
1526
+ > The leading layout is configurable: set `REFORMER_FORM_LAYOUT` (`minimalist` | `folders`)
1527
+ > when registering the MCP server to choose which structure the generators lead with. Default
1528
+ > is `minimalist`.
1429
1529
 
1430
- ## 16. 14.5 UI COMPONENT PATTERNS
1530
+ > Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
1531
+ > renderer-json `renderer.schema.json` + `renderer.behavior.ts` + `registry.ts`), the
1532
+ > centralized-vs-co-located choice, and the full reuse map live in the
1533
+ > **form-directory-layout** guide (`@reformer/mcp`).
1534
+
1535
+ ## 17. 14.5 UI COMPONENT PATTERNS
1431
1536
 
1432
1537
  > **Default rule (read first)**: для UI используй `FormField` из
1433
1538
  > [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
@@ -1612,7 +1717,7 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
1612
1717
  }
1613
1718
  ```
1614
1719
 
1615
- ## 17. 15. NON-EXISTENT API (DO NOT USE)
1720
+ ## 18. 15. NON-EXISTENT API (DO NOT USE)
1616
1721
 
1617
1722
  **Следующего API НЕТ в @reformer/core** (частично — наследие старой path-based архитектуры,
1618
1723
  удалено при переходе на M1):
@@ -1676,7 +1781,166 @@ enableWhen(path.city, (form) => Boolean(form.country));
1676
1781
  enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
1677
1782
  ```
1678
1783
 
1679
- ## 18. 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
1784
+ ## 19. Purpose
1785
+
1786
+ «Показать поле X только если Y» под M1 решается тремя независимыми механизмами — выбирай по
1787
+ тому, что именно должно быть условным:
1788
+
1789
+ 1. **Видимость / доступность** (поле остаётся в модели, но выключается/включается и не валидируется)
1790
+ — behaviors: `enableWhen`, а для условной записи значения — `compute` / `copyFrom` с опцией `{ when }`.
1791
+ 2. **Условная валидация** (правила применяются только в истинной ветке) — нативный branch-узел
1792
+ схемы `{ when, children }`, исполняется `validateFormModel`.
1793
+ 3. **Скрытие из разметки** (узел вообще не рендерится) — `useFormControlValue` + условный рендер в JSX.
1794
+
1795
+ > Ни `applyWhen`, ни `ValidationSchemaFn` **не экспортируются** из `@reformer/core` — см. раздел
1796
+ > [Не экспорт (частая ошибка)](#не-экспорт-частая-ошибка) и `17-nonexistent-api.md`.
1797
+
1798
+ ## 20. Видимость и доступность
1799
+
1800
+ `enableWhen(target, () => condition, { resetOnDisable })` из `@reformer/core/behaviors`
1801
+ включает/выключает поле реактивно. При `disabled` поле не участвует в валидации; с
1802
+ `resetOnDisable: true` его значение сбрасывается при выключении (иначе — сохраняется).
1803
+
1804
+ ```typescript
1805
+ import { defineFormBehavior, enableWhen, compute, copyFrom } from '@reformer/core/behaviors';
1806
+
1807
+ type CreditForm = {
1808
+ loanType: 'mortgage' | 'car' | 'consumer';
1809
+ propertyValue: number | null;
1810
+ initialPayment: number | null;
1811
+ sameEmail: boolean;
1812
+ email: string;
1813
+ emailAdditional: string;
1814
+ };
1815
+
1816
+ export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
1817
+ // один или несколько таргетов; условие читает model.* реактивно
1818
+ enableWhen(
1819
+ [model.$.propertyValue, model.$.initialPayment],
1820
+ () => model.loanType === 'mortgage',
1821
+ { resetOnDisable: true },
1822
+ );
1823
+
1824
+ // условная ЗАПИСЬ производного значения — compute с { when }
1825
+ compute(model.$.initialPayment, () => Math.round((model.propertyValue ?? 0) * 0.2), {
1826
+ when: () => model.loanType === 'mortgage',
1827
+ });
1828
+
1829
+ // условное КОПИРОВАНИЕ из другого поля — copyFrom с { when }
1830
+ copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true });
1831
+ });
1832
+ ```
1833
+
1834
+ Требования:
1835
+
1836
+ - **Все поля должны быть материализованы в модели** (`createModel`), даже те, что показываются
1837
+ только под условием. `enableWhen`/`compute`/`copyFrom` принимают сигналы `model.$.field`, а не
1838
+ строковые пути; сигнала для несуществующего поля нет.
1839
+ - Поведение подключается к форме через `createForm({ model, schema, behavior })` — форма владеет
1840
+ жизненным циклом, DSL-операторы регистрируют свой cleanup сами (ручной массив cleanup'ов не нужен).
1841
+ - Таргетом может быть и **группа**: `enableWhen(model.$.residenceAddress, () => model.sameAsRegistration === false)`
1842
+ проходит по поддереву; без `resetOnDisable` значение группы сохраняется (удобно, когда оно копируется
1843
+ из другого источника).
1844
+
1845
+ > **Групповой таргет работает только у DSL-`enableWhen` из `@reformer/core/behaviors`**, не у
1846
+ > одноимённого корневого `enableWhen` из `@reformer/core` (тот резолвит только leaf-сигналы через
1847
+ > реестр → на group-сигнале это тихий no-op). `get_symbol_docs("enableWhen")` возвращает именно
1848
+ > корневой, не group-capable вариант.
1849
+
1850
+ ## 21. Условная валидация
1851
+
1852
+ Правила, действующие только при выполнении условия, задаются **нативным branch-узлом**
1853
+ `{ when: (scope, root) => boolean, children: [...] }` прямо в дереве схемы. `validateFormModel`
1854
+ обходит дерево: при истинном `when` валидирует `children`, при ложном — **поддерево пропускается,
1855
+ а ошибки его полей очищаются** (движок вызывает `setErrors([])`). `scope` — ближайшая под-модель
1856
+ (элемент массива или корень), `root` — корневая модель формы.
1857
+
1858
+ ```typescript
1859
+ import { validateFormModel } from '@reformer/core';
1860
+ import { required, min } from '@reformer/core/validators';
1861
+
1862
+ const schema = {
1863
+ children: [
1864
+ { value: model.$.loanType, validators: [required()] },
1865
+ {
1866
+ // ветка применяется только для ипотеки
1867
+ when: (_scope, root) => root.loanType === 'mortgage',
1868
+ children: [
1869
+ { value: model.$.propertyValue, validators: [required(), min(1_000_000)] },
1870
+ { value: model.$.initialPayment, validators: [required()] },
1871
+ ],
1872
+ },
1873
+ ],
1874
+ };
1875
+
1876
+ const { valid, errors } = await validateFormModel(model, schema);
1877
+ ```
1878
+
1879
+ Переключение `loanType` с `mortgage` на другой тип автоматически снимает ошибки `required` с
1880
+ `propertyValue`/`initialPayment` — их держать enabled и не валидировать помогает `enableWhen` выше.
1881
+
1882
+ ## 22. Скрытие в JSX
1883
+
1884
+ Чтобы условный блок вообще не рендерился, читай значение-триггер хуком `useFormControlValue(form.field)`
1885
+ и делай ранний `return null`. Хук возвращает **только значение** (без деструктуризации `{ value }`).
1886
+
1887
+ ```tsx
1888
+ import { useFormControlValue } from '@reformer/core';
1889
+
1890
+ function MortgageFields({ form }: { form: CreditForm }) {
1891
+ const loanType = useFormControlValue(form.loanType);
1892
+ if (loanType !== 'mortgage') return null;
1893
+
1894
+ return (
1895
+ <>
1896
+ <PropertyValueField form={form} />
1897
+ <InitialPaymentField form={form} />
1898
+ </>
1899
+ );
1900
+ }
1901
+ ```
1902
+
1903
+ Скрытие в JSX — чисто визуальное: если поле должно ещё и **выпадать из валидации/состояния**,
1904
+ сочетай его с `enableWhen({ resetOnDisable: true })` (доступность) или branch-узлом (валидация).
1905
+
1906
+ ## 23. Не экспорт (частая ошибка)
1907
+
1908
+ - **`applyWhen` — это ЛОКАЛЬНЫЙ typed-хелпер примера, а не экспорт `@reformer/core`.** Он лишь
1909
+ эмитит нативный branch-узел `{ when, children }`. Каноничное определение (см.
1910
+ `complex-multy-step-form/schemas/validation.ts`):
1911
+
1912
+ ```typescript
1913
+ // локальный сахар в самом примере — НЕ импорт из библиотеки
1914
+ const applyWhen = (cond: (form: Root) => boolean, children: SchemaNode[]): SchemaNode => ({
1915
+ when: (_scope: unknown, root: unknown) => cond(root as Root),
1916
+ children,
1917
+ });
1918
+ ```
1919
+
1920
+ Публичный API — сам `validateFormModel(model, schema)` и форма узла `{ when, children }`.
1921
+ Не пиши `import { applyWhen } from '@reformer/core'` — такого символа нет.
1922
+
1923
+ - **`ValidationSchemaFn` не существует** (тип старой path-схемы удалён при переходе на M1). Тип
1924
+ кастомного/cross-field валидатора — **`ModelValidator<TValue, TModel, TRoot>`**:
1925
+
1926
+ ```typescript
1927
+ import type { ModelValidator } from '@reformer/core';
1928
+
1929
+ // (value, scope, root) — cross-field читает соседей через root
1930
+ const passwordsMatch: ModelValidator<string, unknown, { password: string }> = (value, _scope, root) =>
1931
+ value && root.password && value !== root.password
1932
+ ? { code: 'mismatch', message: 'Пароли не совпадают' }
1933
+ : null;
1934
+ ```
1935
+
1936
+ ## 24. See also
1937
+
1938
+ - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры `enableWhen`/`compute`/`copyFrom`, branch-узел, `ModelValidator`
1939
+ - [17-nonexistent-api.md](./17-nonexistent-api.md) — полный список удалённого API (`applyWhen`, `ValidationSchemaFn`, …)
1940
+ - [25-reset-when.md](./25-reset-when.md) — `resetWhen` как альтернатива, когда поле остаётся enabled
1941
+ - [19-reading-values.md](./19-reading-values.md) — `useFormControlValue` и чтение значений в React
1942
+
1943
+ ## 25. 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
1680
1944
 
1681
1945
  Под M1 значения живут в модели. Есть три контекста чтения: value-доступ модели, сигналы, и React-хуки.
1682
1946
 
@@ -1735,7 +1999,7 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
1735
1999
  > цикла нет). Для side-эффектов (загрузка опций, обновление componentProps) — `onChange`.
1736
2000
  > `computeFrom`/`copyFrom`/`enableWhen` принимают сигналы (`model.$.x`), НЕ строковые пути.
1737
2001
 
1738
- ## 19. 17. COMPUTE vs ONCHANGE
2002
+ ## 26. 17. COMPUTE vs ONCHANGE
1739
2003
 
1740
2004
  Под M1 behaviors работают на сигналах модели. Есть два способа их писать:
1741
2005
 
@@ -1776,6 +2040,40 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
1776
2040
  > отследится и пересчёта не будет. Читай поля по отдельности (`model.field` или `model.$.field.value`).
1777
2041
  > `model.get()` — только вне реактивного контекста (в `onChange`, обработчиках событий).
1778
2042
 
2043
+ ### Computed sum over a FormArray
2044
+
2045
+ Агрегат по массиву (сумма доходов созаёмщиков и т.п.) считается **реактивно** через value-proxy
2046
+ массива — `model.<array>.map(...)`:
2047
+
2048
+ ```typescript
2049
+ compute(model.$.coBorrowersIncome, () =>
2050
+ model.coBorrowers.map((cb) => cb.monthlyIncome ?? 0).reduce((s, v) => s + v, 0)
2051
+ );
2052
+ ```
2053
+
2054
+ Почему реактивно: `.map` читает сигнал самого массива (трекает `push`/`removeAt`/reorder) **и**
2055
+ внутри колбэка читает каждый `cb.<field>` (трекает правки элементов) → пересчёт срабатывает на оба
2056
+ вида изменений.
2057
+
2058
+ Ключевой нюанс — **на каком proxy** живёт `.map`/`.reduce`(-via-`.map`):
2059
+
2060
+ - **value-proxy `model.coBorrowers`** — есть `.map`/`.forEach`/`.at`/индексы (обходит элементы как
2061
+ значения). Именно его читай в `compute`.
2062
+ - **signals-proxy `model.$.coBorrowers`** — только индексный доступ и `.length`, без `.map`. Для
2063
+ агрегата не подходит.
2064
+
2065
+ Если нужен только **счётчик** (пересчёт лишь на изменение длины, без чтения полей элементов) —
2066
+ `model.<array>.map(() => null)` (длина трекается, значения — нет):
2067
+
2068
+ ```typescript
2069
+ // зависит только от КОЛИЧЕСТВА элементов, не от их полей
2070
+ compute(model.$.interestRate, () => computeRate(model.properties.map(() => null).length));
2071
+ ```
2072
+
2073
+ > **Не агрегируй через `model.get()` / `model.<array>.peek()`** в `compute` — это нереактивный
2074
+ > снапшот массива, зависимость не отследится и сумма не пересчитается. Читай массив только через
2075
+ > value-proxy `model.<array>.map(...)`.
2076
+
1779
2077
  ### computeFrom — явный список источников
1780
2078
 
1781
2079
  Когда нужен явный контроль зависимостей — `computeFrom(sources, target, fn)`. Значения
@@ -1840,7 +2138,7 @@ const stop = watchField(model.$.country, () => { model.city = ''; });
1840
2138
  порядке через реактивный граф (цель одного = источник другого). Расходящиеся взаимные
1841
2139
  `compute` без стабилизации бросят понятную ошибку (см. `22-cycle-detection.md`).
1842
2140
 
1843
- ## 20. 18. ARRAY OPERATIONS
2141
+ ## 27. 18. ARRAY OPERATIONS
1844
2142
 
1845
2143
  Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);
1846
2144
  рендер — через ноду формы (`form.items.map` / `.at`).
@@ -1948,7 +2246,7 @@ const percentagesSumTo100: ModelValidator<unknown, unknown, MyForm> = (_v, _s, r
1948
2246
  };
1949
2247
  ```
1950
2248
 
1951
- ## 21. Как устроено под M1
2249
+ ## 28. Как устроено под M1
1952
2250
 
1953
2251
  Behaviors на сигналах уже защищены от типичных циклов:
1954
2252
 
@@ -1961,7 +2259,7 @@ Behaviors на сигналах уже защищены от типичных ц
1961
2259
  Тебе НЕ нужно вручную ставить `{ immediate: false }`, guard'ить `disabled.value` перед
1962
2260
  `disable()` или сравнивать значения перед записью — это делается внутри операторов.
1963
2261
 
1964
- ## 22. Когда всё-таки возникает «Cycle detected»
2262
+ ## 29. Когда всё-таки возникает «Cycle detected»
1965
2263
 
1966
2264
  ### 1. Расходящийся взаимный compute
1967
2265
 
@@ -2008,7 +2306,7 @@ resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
2008
2306
  resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
2009
2307
  ```
2010
2308
 
2011
- ## 23. Prefer built-in operators over manual logic
2309
+ ## 30. Prefer built-in operators over manual logic
2012
2310
 
2013
2311
  Вместо ручной сборки reset-on-disable через `onChange` — используй `enableWhen({ resetOnDisable })`:
2014
2312
 
@@ -2026,21 +2324,21 @@ enableWhen([model.$.propertyValue, model.$.initialPayment], () => model.loanType
2026
2324
  });
2027
2325
  ```
2028
2326
 
2029
- ## 24. Key Rules
2327
+ ## 31. Key Rules
2030
2328
 
2031
2329
  1. Производные значения — через `compute`/`computeFrom` (peek-guard встроен), не ручным `onChange` + запись.
2032
2330
  2. `transformValue`-трансформер должен быть идемпотентным (`f(f(x)) === f(x)`).
2033
2331
  3. Условие `resetWhen`/`copyFrom`/`enableWhen` НЕ должно читать собственную цель.
2034
2332
  4. Расходящийся взаимный `compute` — разорви однонаправленной зависимостью или `when`.
2035
2333
 
2036
- ## 25. Purpose
2334
+ ## 32. Purpose
2037
2335
 
2038
2336
  `copyFrom` декларативно копирует значение одного поля (или группы) в другое при выполнении
2039
2337
  условия `when`. Используется для UX-сценариев «совпадает с …»: «адрес проживания = адрес
2040
2338
  регистрации», «email для уведомлений = основной email», «billing = shipping». Оператор сам
2041
2339
  выходит из reactive-контекста (`runOutsideEffect`), поэтому не порождает «Cycle detected».
2042
2340
 
2043
- ## 26. API
2341
+ ## 33. API
2044
2342
 
2045
2343
  Есть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):
2046
2344
 
@@ -2065,7 +2363,7 @@ function copyFrom<T>(
2065
2363
  `when` — реактивное условие (читает сигналы модели через `model.*`). `transform` применяется
2066
2364
  к значению перед записью в target. Отдельной опции `fields`/`debounce` нет.
2067
2365
 
2068
- ## 27. Examples
2366
+ ## 34. Examples
2069
2367
 
2070
2368
  ### Базовый сценарий — синхронизация двух адресов
2071
2369
 
@@ -2133,7 +2431,7 @@ const cleanups = [
2133
2431
  // teardown: cleanups.forEach((c) => c());
2134
2432
  ```
2135
2433
 
2136
- ## 28. Anti-patterns
2434
+ ## 35. Anti-patterns
2137
2435
 
2138
2436
  ```typescript
2139
2437
  // ❌ Двусторонняя связь через два copyFrom — конфликт направлений
@@ -2152,7 +2450,7 @@ copyFrom(model.$.source, model.$.target, { when: () => model.target === '' });
2152
2450
  copyFrom(model.$.source, model.$.target, { when: () => model.copyEnabled === true });
2153
2451
  ```
2154
2452
 
2155
- ## 29. Troubleshooting
2453
+ ## 36. Troubleshooting
2156
2454
 
2157
2455
  **Q: Скопированное значение не появляется в target.**
2158
2456
  A: Проверьте, что (1) behavior передан в `createForm({ behavior })` (или примитив вызван и не
@@ -2166,14 +2464,14 @@ source и независимых флагов.
2166
2464
  A: `copyFrom` при `when === false` просто не пишет (не сбрасывает). Для сброса используй параллельно
2167
2465
  `resetWhen(model.$.target, () => !model.copyEnabled)` (см. `25-reset-when.md`).
2168
2466
 
2169
- ## 30. See also
2467
+ ## 37. See also
2170
2468
 
2171
2469
  - [24-sync-fields.md](./24-sync-fields.md) — двусторонняя синхронизация
2172
2470
  - [25-reset-when.md](./25-reset-when.md) — сброс target при выключенном условии
2173
2471
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
2174
2472
  - [22-cycle-detection.md](./22-cycle-detection.md) — почему `runOutsideEffect` защищает от циклов
2175
2473
 
2176
- ## 31. Purpose
2474
+ ## 38. Purpose
2177
2475
 
2178
2476
  `syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них
2179
2477
  переписывает второе. Применяется для дублей одного значения в разных частях формы
@@ -2181,7 +2479,7 @@ A: `copyFrom` при `when === false` просто не пишет (не сбр
2181
2479
  исключают петли. Для одностороннего копирования — [`copyFrom`](./23-copy-from.md); для
2182
2480
  расчётов — [`compute`](./20-compute-vs-watch.md).
2183
2481
 
2184
- ## 32. API
2482
+ ## 39. API
2185
2483
 
2186
2484
  Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
2187
2485
 
@@ -2196,7 +2494,7 @@ function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (valu
2196
2494
  `a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:
2197
2495
  применяется только при движении значения `a → b`. Опции `debounce`/`when` нет.
2198
2496
 
2199
- ## 33. Examples
2497
+ ## 40. Examples
2200
2498
 
2201
2499
  ### Базовый сценарий — отзеркаливание текста
2202
2500
 
@@ -2235,7 +2533,7 @@ const stop = syncFields(model.$.syncField1, model.$.syncField2);
2235
2533
  // stop() — отписаться
2236
2534
  ```
2237
2535
 
2238
- ## 34. Anti-patterns
2536
+ ## 41. Anti-patterns
2239
2537
 
2240
2538
  ```typescript
2241
2539
  // ❌ Симметрично через два copyFrom — конфликт направлений
@@ -2265,7 +2563,7 @@ syncFields(model.$.amountString, model.$.amountNumber); // string ↔ number
2265
2563
  compute(model.$.amountNumber, () => Number(model.amountString));
2266
2564
  ```
2267
2565
 
2268
- ## 35. Troubleshooting
2566
+ ## 42. Troubleshooting
2269
2567
 
2270
2568
  **Q: Поля «дёргаются», несколько перезаписей.**
2271
2569
  A: Чаще всего на одном из полей висит `transformValue`/`compute`. Убедитесь, что transform
@@ -2279,21 +2577,21 @@ A: Не вешайте дополнительно `onChange`/`watchField`, ко
2279
2577
  A: У `syncFields` нет `when`. Эмулируй через два `copyFrom(a→b, { when })` / `copyFrom(b→a, { when })`
2280
2578
  с флагами, разрешающими только одну активную сторону, либо через `apply` под условием.
2281
2579
 
2282
- ## 36. See also
2580
+ ## 43. See also
2283
2581
 
2284
2582
  - [23-copy-from.md](./23-copy-from.md) — однонаправленное копирование с `when`
2285
2583
  - [26-transform-value.md](./26-transform-value.md) — нормализация значений на месте
2286
2584
  - [22-cycle-detection.md](./22-cycle-detection.md) — почему симметричный copy ломается
2287
2585
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
2288
2586
 
2289
- ## 37. Purpose
2587
+ ## 44. Purpose
2290
2588
 
2291
2589
  `resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это
2292
2590
  альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но
2293
2591
  содержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но
2294
2592
  поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение.
2295
2593
 
2296
- ## 38. API
2594
+ ## 45. API
2297
2595
 
2298
2596
  Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
2299
2597
 
@@ -2309,7 +2607,7 @@ function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { r
2309
2607
  `resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
2310
2608
  поля не трогаются оператором (значения принадлежат модели).
2311
2609
 
2312
- ## 39. Examples
2610
+ ## 46. Examples
2313
2611
 
2314
2612
  ### Базовый сценарий — сброс номера карты при смене способа оплаты
2315
2613
 
@@ -2345,7 +2643,7 @@ import { resetWhen } from '@reformer/core';
2345
2643
  const stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
2346
2644
  ```
2347
2645
 
2348
- ## 40. Anti-patterns
2646
+ ## 47. Anti-patterns
2349
2647
 
2350
2648
  ```typescript
2351
2649
  // ❌ resetWhen вместо enableWhen для disable-сценария
@@ -2380,7 +2678,7 @@ resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
2380
2678
  resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
2381
2679
  ```
2382
2680
 
2383
- ## 41. Troubleshooting
2681
+ ## 48. Troubleshooting
2384
2682
 
2385
2683
  **Q: Сброс не срабатывает, хотя `condition` возвращает true.**
2386
2684
  A: Проверьте, что behavior зарегистрирован (`createForm({ behavior })`) или примитив не отписан,
@@ -2394,13 +2692,13 @@ A: Убедитесь, что `condition` не зависит от значен
2394
2692
  A: `resetWhen` рассчитан на скалярные сигналы. Для группы используй `enableWhen({ resetOnDisable: true })`
2395
2693
  (проходит по поддереву) или `model.reset()` для сброса всей формы к initial-снимку.
2396
2694
 
2397
- ## 42. See also
2695
+ ## 49. See also
2398
2696
 
2399
2697
  - [04-common-patterns.md](./04-common-patterns.md) — `enableWhen({ resetOnDisable: true })` как альтернатива
2400
2698
  - [23-copy-from.md](./23-copy-from.md) — копирование, у которого нет встроенного отката
2401
2699
  - [22-cycle-detection.md](./22-cycle-detection.md) — почему `condition` не должен читать целевое поле
2402
2700
 
2403
- ## 43. Purpose
2701
+ ## 50. Purpose
2404
2702
 
2405
2703
  `transformValue` подписывается на изменение поля и переписывает его трансформированной
2406
2704
  версией: uppercase для кодов, trim+toLowerCase для email, округление для чисел. Применяется
@@ -2409,7 +2707,7 @@ A: `resetWhen` рассчитан на скалярные сигналы. Для
2409
2707
  Оператор откладывает запись вне effect-контекста (`runOutsideEffect`) и не пишет, если
2410
2708
  `transformer(value) === value` — базовый guard от циклов.
2411
2709
 
2412
- ## 44. API
2710
+ ## 51. API
2413
2711
 
2414
2712
  Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
2415
2713
 
@@ -2425,7 +2723,7 @@ function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): voi
2425
2723
  Дополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора
2426
2724
  `transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.
2427
2725
 
2428
- ## 45. Examples
2726
+ ## 52. Examples
2429
2727
 
2430
2728
  ### Базовый сценарий — uppercase для кода
2431
2729
 
@@ -2487,7 +2785,7 @@ toUpper(model.$.promoCode);
2487
2785
  trim(model.$.username);
2488
2786
  ```
2489
2787
 
2490
- ## 46. Anti-patterns
2788
+ ## 53. Anti-patterns
2491
2789
 
2492
2790
  ```typescript
2493
2791
  // ❌ Неидемпотентный transformer — бесконечный цикл
@@ -2515,7 +2813,7 @@ transformValue(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
2515
2813
  compute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
2516
2814
  ```
2517
2815
 
2518
- ## 47. Troubleshooting
2816
+ ## 54. Troubleshooting
2519
2817
 
2520
2818
  **Q: «Cycle detected» при transformValue.**
2521
2819
  A: 99% — неидемпотентный transformer. Проверь `transformer(transformer(x)) === transformer(x)`.
@@ -2528,14 +2826,14 @@ A: Проверь, что behavior зарегистрирован (`createForm({
2528
2826
  A: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)
2529
2827
  лучше делать через `<InputMask>` из `@reformer/ui-kit`, а не `transformValue`.
2530
2828
 
2531
- ## 48. See also
2829
+ ## 55. See also
2532
2830
 
2533
2831
  - [24-sync-fields.md](./24-sync-fields.md) — порядок применения с syncFields
2534
2832
  - [23-copy-from.md](./23-copy-from.md) — `transform`-опция при копировании
2535
2833
  - [22-cycle-detection.md](./22-cycle-detection.md) — про идемпотентность
2536
2834
  - [16-ui-components.md](./16-ui-components.md) — `<InputMask>` для тяжёлого форматирования
2537
2835
 
2538
- ## 49. Purpose
2836
+ ## 56. Purpose
2539
2837
 
2540
2838
  `revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
2541
2839
  зависимостей. Применяется, когда правило зависит от значений других полей
@@ -2543,7 +2841,7 @@ A: Симптом частых записей. Форматирование с
2543
2841
  Под M1 валидация on-demand (`validateFormModel`), поэтому ревалидация выражается явным
2544
2842
  колбэком, а не автоматической привязкой к полю.
2545
2843
 
2546
- ## 50. API
2844
+ ## 57. API
2547
2845
 
2548
2846
  Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
2549
2847
 
@@ -2559,7 +2857,7 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
2559
2857
  `() => validateFormModel(model, schema)`). Вызывается при изменении любой зависимости
2560
2858
  (НЕ на инициализации), вне effect-контекста.
2561
2859
 
2562
- ## 51. Examples
2860
+ ## 58. Examples
2563
2861
 
2564
2862
  ### Базовый сценарий — перевалидация amount при смене maxAmount
2565
2863
 
@@ -2612,7 +2910,7 @@ import { revalidateWhen, validateFormModel } from '@reformer/core';
2612
2910
  const stop = revalidateWhen([model.$.maxAmount], () => void validateFormModel(model, schema));
2613
2911
  ```
2614
2912
 
2615
- ## 52. Anti-patterns
2913
+ ## 59. Anti-patterns
2616
2914
 
2617
2915
  ```typescript
2618
2916
  // ❌ Триггер == поле, которое и так меняется само
@@ -2635,7 +2933,7 @@ const amountVsMax: ModelValidator<number, unknown, Form> = (v, _s, root) =>
2635
2933
  revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
2636
2934
  ```
2637
2935
 
2638
- ## 53. Troubleshooting
2936
+ ## 60. Troubleshooting
2639
2937
 
2640
2938
  **Q: Ошибка target не пропадает после изменения триггера.**
2641
2939
  A: Проверьте, что (1) правило реально читает значение триггера через `root` (cross-field
@@ -2645,20 +2943,20 @@ A: Проверьте, что (1) правило реально читает з
2645
2943
  **Q: Как перевалидировать только одно поле, а не всю схему?**
2646
2944
  A: Передай в колбэк под-схему только этого поля: `validateFormModel(model, { children: [fieldNode] })`.
2647
2945
 
2648
- ## 54. See also
2946
+ ## 61. See also
2649
2947
 
2650
2948
  - [03-api-signatures.md](./03-api-signatures.md) — cross-field `ModelValidator` и `validateFormModel`
2651
2949
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
2652
2950
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения
2653
2951
 
2654
- ## 55. Purpose
2952
+ ## 62. Purpose
2655
2953
 
2656
2954
  Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateFormModel`) →
2657
2955
  проверить `result.valid` → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
2658
2956
  Валидация — чистая функция ДАННЫХ (модели), она же роутит ошибки в ноды формы, поэтому UI
2659
2957
  подсветит проблемные поля. `model.reset()` возвращает значения к initial-снимку.
2660
2958
 
2661
- ## 56. API
2959
+ ## 63. API
2662
2960
 
2663
2961
  ```typescript
2664
2962
  // Модель (источник истины значений):
@@ -2680,7 +2978,7 @@ form.<field>.markAsTouched();
2680
2978
  form.<field>.reset();
2681
2979
  ```
2682
2980
 
2683
- ## 57. Examples
2981
+ ## 64. Examples
2684
2982
 
2685
2983
  ### Базовый submit-handler
2686
2984
 
@@ -2793,7 +3091,7 @@ async function processFormPayload(rawData: Partial<MyForm>) {
2793
3091
  }
2794
3092
  ```
2795
3093
 
2796
- ## 58. Anti-patterns
3094
+ ## 65. Anti-patterns
2797
3095
 
2798
3096
  ```typescript
2799
3097
  // ❌ Чтение результата валидации без await
@@ -2833,7 +3131,7 @@ model.password = '';
2833
3131
  model.reset();
2834
3132
  ```
2835
3133
 
2836
- ## 59. Troubleshooting
3134
+ ## 66. Troubleshooting
2837
3135
 
2838
3136
  **Q: После `reset()` в UI остались старые ошибки.**
2839
3137
  A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
@@ -2848,14 +3146,14 @@ A: `reset()` возвращает к initial-снимку (значения на
2848
3146
  A: `model.get()` возвращает значения всех полей. Фильтруй вручную после `get()` или используй
2849
3147
  `enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial.
2850
3148
 
2851
- ## 60. See also
3149
+ ## 67. See also
2852
3150
 
2853
3151
  - [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
2854
3152
  - [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateFormModel`
2855
3153
  - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateFormModel`
2856
3154
  - [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки
2857
3155
 
2858
- ## 61. Purpose
3156
+ ## 68. Purpose
2859
3157
 
2860
3158
  Загрузка формы данными с сервера под M1: (1) **initial values** в `createModel(...)`; (2)
2861
3159
  **`model.set` / `model.patch`** для загрузки/обновления значений; (3) **external React-hook**
@@ -2864,7 +3162,7 @@ race-guard через deps). Динамические `componentProps` (опци
2864
3162
  `form.field.updateComponentProps({ options })` в `queueMicrotask`, чтобы не пересечься с
2865
3163
  реактивными эффектами от `set`/`patch`.
2866
3164
 
2867
- ## 62. API
3165
+ ## 69. API
2868
3166
 
2869
3167
  ```typescript
2870
3168
  // Модель:
@@ -2880,7 +3178,7 @@ form.field.updateComponentProps(props: Record<string, unknown>): void; // дин
2880
3178
  Initial values задаются в `createModel(initial)`; `reset()` возвращает к ним. Чтобы загруженные
2881
3179
  данные стали новой «точкой отсчёта» для `reset()` — вызови `model.captureInitial()` после load.
2882
3180
 
2883
- ## 63. Examples
3181
+ ## 70. Examples
2884
3182
 
2885
3183
  ### Initial values в модели
2886
3184
 
@@ -2987,7 +3285,7 @@ export const addressBehavior = defineFormBehavior<AddressForm>(({ model, form })
2987
3285
  });
2988
3286
  ```
2989
3287
 
2990
- ## 64. Anti-patterns
3288
+ ## 71. Anti-patterns
2991
3289
 
2992
3290
  ```typescript
2993
3291
  // ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз
@@ -3027,7 +3325,7 @@ useEffect(() => {
3027
3325
  }, [id]);
3028
3326
  ```
3029
3327
 
3030
- ## 65. Troubleshooting
3328
+ ## 72. Troubleshooting
3031
3329
 
3032
3330
  **Q: После `set`/`patch` не показываются ошибки.**
3033
3331
  A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateFormModel(model, schema)`.
@@ -3040,14 +3338,14 @@ A: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что
3040
3338
  A: `reset()` возвращает к initial-снимку из `createModel`, не к последнему `set`. Чтобы «сбросить
3041
3339
  к данным с сервера» — после `set` вызови `model.captureInitial()` (новая точка отсчёта).
3042
3340
 
3043
- ## 66. See also
3341
+ ## 73. See also
3044
3342
 
3045
3343
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — обратная сторона жизненного цикла
3046
3344
  - [11-async-watchfield.md](./11-async-watchfield.md) — `onChange` для динамики после preload
3047
3345
  - [22-cycle-detection.md](./22-cycle-detection.md) — почему `queueMicrotask` нужен
3048
3346
  - [16-ui-components.md](./16-ui-components.md) — `updateComponentProps` для динамических опций
3049
3347
 
3050
- ## 67. 30. TYPE-SAFETY RECIPES
3348
+ ## 74. 30. TYPE-SAFETY RECIPES
3051
3349
 
3052
3350
  Идиоматичные паттерны, которые держат сгенерированный код без `any` и `as`-кастов под M1.
3053
3351
 
@@ -3198,7 +3496,7 @@ const schema = {
3198
3496
  - `as`-касты значений полей внутри `compute` → читай `model.field` напрямую (Recipe 5).
3199
3497
  - строковые пути / `(form) => ...` в behaviors → это удалённый API, используй сигналы (`model.$.x`).
3200
3498
 
3201
- ## 68. 31. ASYNC VALIDATOR
3499
+ ## 75. 31. ASYNC VALIDATOR
3202
3500
 
3203
3501
  Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
3204
3502
  async-валидатор это `ModelValidator`, возвращающий `Promise<ValidationError | null>`. Он
@@ -3272,7 +3570,7 @@ return pending ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
3272
3570
  - [29-async-preload.md](29-async-preload.md) — async preload данных при init формы
3273
3571
  - [03-api-signatures.md](03-api-signatures.md) — `ModelValidator` и `validateFormModel`
3274
3572
 
3275
- ## 69. 32. ASYNC OPTIONS LOADING
3573
+ ## 76. 32. ASYNC OPTIONS LOADING
3276
3574
 
3277
3575
  Для динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,
3278
3576
  `carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +
@@ -3381,7 +3679,7 @@ loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: tr
3381
3679
  - [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
3382
3680
  - [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)
3383
3681
 
3384
- ## 70. API Reference
3682
+ ## 77. API Reference
3385
3683
 
3386
3684
  _Auto-generated from JSDoc on public exports._
3387
3685
 
@@ -3407,7 +3705,7 @@ _Source: src/core/types/index.ts_
3407
3705
 
3408
3706
  **Signature:**
3409
3707
  ```typescript
3410
- export interface ArrayConfig<T extends FormFields> {
3708
+ export interface ArrayConfig<T extends object> {
3411
3709
  itemSchema: FormSchema<T>;
3412
3710
  initial?: Partial<T>[];
3413
3711
  }
@@ -3517,6 +3815,23 @@ export interface ArrayControlState<T> {
3517
3815
  * ```
3518
3816
  */
3519
3817
  dirty: boolean;
3818
+
3819
+ /**
3820
+ * Флаг отключения массива.
3821
+ * `true` когда массив отключён (`ArrayNode.disable()`), в том числе через
3822
+ * распространение disable от родительской группы.
3823
+ *
3824
+ * Используйте для отключения UI-действий добавления/удаления, когда массив
3825
+ * структурно неизменяем.
3826
+ *
3827
+ * @example
3828
+ * ```tsx
3829
+ * const { disabled } = useFormControl(itemsArray);
3830
+ *
3831
+ * return <button disabled={disabled} onClick={() => control.push()}>Add</button>;
3832
+ * ```
3833
+ */
3834
+ disabled: boolean;
3520
3835
  }
3521
3836
  ```
3522
3837
 
@@ -3614,19 +3929,22 @@ _Source: src/core/types/index.ts_
3614
3929
 
3615
3930
  **Kind:** `type`
3616
3931
 
3617
- Чистый асинхронный валидатор поля.
3618
-
3619
- Регистрируется через `validateAsync(path, validator, { debounce })`.
3932
+ Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
3933
+ {@link Validator}, но возвращает `Promise`.
3620
3934
 
3621
3935
  **Signature:**
3622
3936
  ```typescript
3623
3937
  export type AsyncValidator<TForm, TField> = (
3624
3938
  value: TField,
3625
- control: FormProxy<TField>,
3626
- root: FormProxy<TForm>
3939
+ scope: unknown,
3940
+ root: FormModel<TForm>
3627
3941
  ) => Promise<ValidationError | null>;
3628
3942
  ```
3629
3943
 
3944
+ **Deprecated:** Осиротевший остаток удалённого оператора `validateAsync` (Ф7). Рантаймом не
3945
+ потребляется — живой async-путь узла поля использует `AsyncValidatorFn` `(value, { signal })`.
3946
+ Экспортируется только ради обратной совместимости.
3947
+
3630
3948
  _Source: src/core/types/validation-schema.ts_
3631
3949
 
3632
3950
  ### AsyncValidatorFn
@@ -3736,13 +4054,16 @@ _Source: src/core/model/behaviors.ts_
3736
4054
 
3737
4055
  **Kind:** `type`
3738
4056
 
3739
- Функция условия для applyWhen.
4057
+ Функция-предикат `(value) => boolean`.
3740
4058
 
3741
4059
  **Signature:**
3742
4060
  ```typescript
3743
4061
  export type ConditionFn<T> = (value: T) => boolean;
3744
4062
  ```
3745
4063
 
4064
+ **Deprecated:** Осиротевший остаток удалённого оператора `applyWhen` (Ф7). Рантаймом не
4065
+ потребляется; экспортируется только ради обратной совместимости.
4066
+
3746
4067
  _Source: src/core/types/validation-schema.ts_
3747
4068
 
3748
4069
  ### ConfigWithSchema
@@ -3925,10 +4246,10 @@ export interface CreateFormFromModelArgs<T> {
3925
4246
  /** Реактивная модель данных (источник истины значений). */
3926
4247
  model: FormModel<T>;
3927
4248
  /**
3928
- * Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг поля к ноде
3929
- * по идентичности сигнала (`node.value === model.$.path`). Опциональна.
4249
+ * Единая Schema (дерево узлов {@link FormSchemaNode}). createForm обходит её и привязывает конфиг
4250
+ * поля к ноде по идентичности сигнала (`node.value === model.$.path`). Опциональна.
3930
4251
  */
3931
- schema?: unknown;
4252
+ schema?: FormSchemaNode;
3932
4253
  /**
3933
4254
  * Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
3934
4255
  * заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
@@ -4048,6 +4369,15 @@ _Source: src/core/validation/validators/email.ts_
4048
4369
  ⚠️ Поле должно быть материализовано в форме (`createForm`) — иначе ноды в реестре нет (например,
4049
4370
  элемент массива, который строится per-item).
4050
4371
 
4372
+ ⚠️ Если то же поле одновременно является целью `compute`/`computeFrom`, обязательно ограничь
4373
+ compute тем же условием (`when`), что и это `enableWhen`. Механизмы не согласованы: `enableWhen`
4374
+ работает со статус-машиной ноды (enable/disable/reset), а `compute` пишет `target.value` напрямую
4375
+ в сигнал модели и проверяет только собственный `options.when` — состояние `disabled` он НЕ смотрит.
4376
+ Поэтому при выключенном поле любое изменение зависимости compute молча заново заполнит его (а с
4377
+ `resetOnDisable` — перезапишет только что сброшенное значение), и так как `getValue()`/`submit`
4378
+ включают disabled-поля, мусор утечёт в payload. Это аналог правила #13 гайда add-behavior,
4379
+ которое сегодня сформулировано только для `copyFrom`; для `compute` то же ограничение обязательно.
4380
+
4051
4381
  **Signature:**
4052
4382
  ```typescript
4053
4383
  export function enableWhen(
@@ -4061,6 +4391,12 @@ export function enableWhen(
4061
4391
 
4062
4392
  ```typescript
4063
4393
  enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', { resetOnDisable: true });
4394
+
4395
+ // Поле-цель compute, которое ТАКЖЕ в resetOnDisable enableWhen: compute обязан нести тот же when,
4396
+ // иначе initialPayment заново заполнится в выключенном состоянии и утечёт в submit.
4397
+ compute(model.$.initialPayment, () => model.propertyValue * 0.2, {
4398
+ when: () => model.loanType === 'mortgage',
4399
+ });
4064
4400
  ```
4065
4401
 
4066
4402
  _Source: src/core/model/behaviors.ts_
@@ -4081,7 +4417,7 @@ export interface ErrorFilterOptions {
4081
4417
  message?: string;
4082
4418
 
4083
4419
  /** Фильтр по параметрам ошибки */
4084
- params?: FormFields;
4420
+ params?: Record<string, FormValue>;
4085
4421
 
4086
4422
  /** Кастомный предикат для фильтрации */
4087
4423
  predicate?: (error: ValidationError) => boolean;
@@ -4334,8 +4670,22 @@ export interface FieldControlState<T> {
4334
4670
  * );
4335
4671
  * ```
4336
4672
  */
4337
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4338
- componentProps: Record<string, any>;
4673
+ componentProps: Record<string, unknown>;
4674
+
4675
+ /**
4676
+ * Флаг изменения поля.
4677
+ * `true` когда значение поля отличается от начального.
4678
+ *
4679
+ * Реактивный аналог `control.dirty` — паритет с {@link ArrayControlState.dirty}.
4680
+ *
4681
+ * @example
4682
+ * ```tsx
4683
+ * const { dirty } = useFormControl(field);
4684
+ *
4685
+ * return dirty ? <span className="badge">Изменено</span> : null;
4686
+ * ```
4687
+ */
4688
+ dirty: boolean;
4339
4689
  }
4340
4690
  ```
4341
4691
 
@@ -4599,22 +4949,6 @@ try {
4599
4949
 
4600
4950
  _Source: src/core/utils/error-handler.ts_
4601
4951
 
4602
- ### FormFields
4603
-
4604
- **Kind:** `type`
4605
-
4606
- Тип для конфига с полями (FormSchema generic constraint)
4607
- Используется вместо `Record<string, any>` для схем форм
4608
-
4609
- **Signature:**
4610
- ```typescript
4611
- export type FormFields = Record<string, FormValue>;
4612
- ```
4613
-
4614
- **Deprecated:** (no message)
4615
-
4616
- _Source: src/core/types/index.ts_
4617
-
4618
4952
  ### FormModel
4619
4953
 
4620
4954
  **Kind:** `type`
@@ -4679,7 +5013,27 @@ _Source: src/core/nodes/form-node.ts_
4679
5013
 
4680
5014
  **Signature:**
4681
5015
  ```typescript
4682
- export type FormProxy<T> = GroupNode<T> & FormControlsProxy<T>;
5016
+ export type FormProxy<T> = GroupNode<T> &
5017
+ // Исключаем имена, совпадающие с членами GroupNode: для них `form.<name>` возвращает член узла,
5018
+ // а не поле. Оставлять их в полевом пространстве — значит пересекать, напр., ReadonlySignal<FieldStatus>
5019
+ // с FieldNode<string> и молча компилировать невозможный тип. Escape-hatch — `form.$` ниже.
5020
+ Omit<FormControlsProxy<T>, keyof GroupNode<T>> & {
5021
+ /**
5022
+ * Escape-hatch пространство имён «controls»: типобезопасный доступ ко ВСЕМ полям формы
5023
+ * по имени, включая поля, чьи имена совпадают с членами {@link GroupNode}
5024
+ * (`value`/`status`/`id`/`errors`/…), недостижимые через `form.<name>`.
5025
+ *
5026
+ * @example
5027
+ * ```typescript
5028
+ * // модель: { status: string; email: string }
5029
+ * form.status; // ReadonlySignal<FieldStatus> — агрегат GroupNode (не поле!)
5030
+ * form.$.status; // FieldNode<string> — поле пользователя
5031
+ * form.$.status.setValue('active');
5032
+ * form.$.email; // FieldNode<string> — так же доступны и незатенённые поля
5033
+ * ```
5034
+ */
5035
+ readonly $: FormControlsProxy<T>;
5036
+ };
4683
5037
  ```
4684
5038
 
4685
5039
  **Examples:**
@@ -4711,26 +5065,33 @@ _Source: src/core/types/form-proxy.ts_
4711
5065
 
4712
5066
  **Kind:** `type`
4713
5067
 
4714
- Автоматически определяет тип схемы на основе TypeScript типа:
5068
+ **Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
5069
+ (или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
4715
5070
  - `T[] -> [FormSchema<T>]` (массив с одним элементом)
4716
5071
  - `object -> FormSchema<T>` (группа)
4717
5072
  - `primitive -> FieldConfig<T>` (поле)
4718
5073
 
4719
- Использует NonNullable для корректной обработки опциональных полей
5074
+ Использует NonNullable для корректной обработки опциональных полей.
5075
+
5076
+ ⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
5077
+ контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
5078
+ (ключи = поля данных), а не узел дерева.
4720
5079
 
4721
5080
  **Signature:**
4722
5081
  ```typescript
4723
5082
  export type FormSchema<T> = {
5083
+ // Листовые позиции используют NonEmptyFieldConfig: пустой `{}` (частая опечатка) отклоняется
5084
+ // на этапе компиляции, но каждое свойство FieldConfig по отдельности остаётся опциональным.
4724
5085
  [K in keyof T]: NonNullable<T[K]> extends string | number | boolean
4725
- ? FieldConfig<T[K]>
5086
+ ? NonEmptyFieldConfig<T[K]>
4726
5087
  : NonNullable<T[K]> extends Array<infer U>
4727
5088
  ? U extends string | number | boolean
4728
- ? FieldConfig<T[K]>
5089
+ ? NonEmptyFieldConfig<T[K]>
4729
5090
  : U extends Date | File | Blob | AnyFunction
4730
- ? FieldConfig<T[K]>
5091
+ ? NonEmptyFieldConfig<T[K]>
4731
5092
  : [FormSchema<U>]
4732
5093
  : NonNullable<T[K]> extends Date | File | Blob | AnyFunction
4733
- ? FieldConfig<T[K]>
5094
+ ? NonEmptyFieldConfig<T[K]>
4734
5095
  : FormSchema<NonNullable<T[K]>>;
4735
5096
  };
4736
5097
  ```
@@ -4765,6 +5126,65 @@ const schema: FormSchema<Form> = {
4765
5126
 
4766
5127
  _Source: src/core/types/deep-schema.ts_
4767
5128
 
5129
+ ### FormSchemaNode
5130
+
5131
+ **Kind:** `interface`
5132
+
5133
+ Узел единой схемы M1 — дерево, обходимое `createForm({ model, schema })`,
5134
+ `validateModel`/`validateFormModel` и рендерерами.
5135
+
5136
+ Узел совмещает несколько ролей (различаются рантаймом по форме):
5137
+ - **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
5138
+ - **массив** — `{ array: model.<path>, item(itemModel) }`;
5139
+ - **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
5140
+ - **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).
5141
+
5142
+ Индексная сигнатура (`[key: string]: unknown`) отражает свободный рекурсивный обход: под-узлы
5143
+ допустимы под любым ключом. Известные поля типизированы (даёт автокомплит и проверку их типов).
5144
+
5145
+ **Signature:**
5146
+ ```typescript
5147
+ export interface FormSchemaNode {
5148
+ /**
5149
+ * «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма
5150
+ * зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до
5151
+ * `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
5152
+ */
5153
+ value?: unknown;
5154
+ /** UI-компонент. Опционален: core-часть работает без UI (значение/валидация). */
5155
+ component?: ComponentType<any>;
5156
+ /** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
5157
+ componentProps?: Record<string, unknown>;
5158
+ validators?: SchemaValidator[];
5159
+ asyncValidators?: SchemaValidator[];
5160
+ updateOn?: 'change' | 'blur' | 'submit';
5161
+ disabled?: boolean;
5162
+ /** Задержка (мс) перед запуском асинхронной валидации. */
5163
+ debounce?: number;
5164
+ /** Идентификатор узла (для wizard/tabs/renderBehavior). */
5165
+ selector?: string;
5166
+ testId?: string;
5167
+ /** Дочерние узлы (даёт контекстную типизацию вложенным литералам — value/validators/when). */
5168
+ children?: readonly FormSchemaNode[];
5169
+ /** Условие включения поддерева (branch-узел `{ when, children }`). */
5170
+ when?: (scope: any, root: any) => boolean;
5171
+ /** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */
5172
+ array?: SchemaArrayControl;
5173
+ /** Схема элемента массива: под-модель элемента → узел поддерева. */
5174
+ item?: (itemModel: any) => FormSchemaNode;
5175
+ /**
5176
+ * Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,
5177
+ * либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`
5178
+ * схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.
5179
+ */
5180
+ initialValue?: unknown;
5181
+ /** Свободная вложенность: record-of-fields и произвольные под-узлы. */
5182
+ [key: string]: unknown;
5183
+ }
5184
+ ```
5185
+
5186
+ _Source: src/core/types/schema-node.ts_
5187
+
4768
5188
  ### FormStatusMachine
4769
5189
 
4770
5190
  **Kind:** `class`
@@ -4810,7 +5230,7 @@ FormSubmitter - управляет процессом отправки форм
4810
5230
 
4811
5231
  **Signature:**
4812
5232
  ```typescript
4813
- export class FormSubmitter<T extends FormFields> {
5233
+ export class FormSubmitter<T extends object> {
4814
5234
  /** Внутренний сигнал состояния отправки */ /* … */ }
4815
5235
  ```
4816
5236
 
@@ -5018,7 +5438,7 @@ _Source: src/core/types/index.ts_
5018
5438
 
5019
5439
  **Signature:**
5020
5440
  ```typescript
5021
- export function integer<TForm = unknown, TField extends number | undefined = number>(
5441
+ export function integer<TForm = unknown, TField extends number | null | undefined = number>(
5022
5442
  options?: ValidateOptions
5023
5443
  ): Validator<TForm, TField>
5024
5444
  ```
@@ -5055,7 +5475,7 @@ ArrayNode представляет массив вложенных форм (о
5055
5475
 
5056
5476
  **Signature:**
5057
5477
  ```typescript
5058
- export function isArrayNode(value: unknown): value is ArrayNode<FormFields>
5478
+ export function isArrayNode(value: unknown): value is ArrayNode<object>
5059
5479
  ```
5060
5480
 
5061
5481
  **Parameters:**
@@ -5217,7 +5637,7 @@ GroupNode представляет объект с вложенными поля
5217
5637
 
5218
5638
  **Signature:**
5219
5639
  ```typescript
5220
- export function isGroupNode(value: unknown): value is GroupNode<FormFields>
5640
+ export function isGroupNode(value: unknown): value is GroupNode<object>
5221
5641
  ```
5222
5642
 
5223
5643
  **Parameters:**
@@ -5247,7 +5667,7 @@ _Source: src/core/utils/type-guards.ts_
5247
5667
 
5248
5668
  **Signature:**
5249
5669
  ```typescript
5250
- export function isNumber<TForm = unknown, TField extends number | undefined = number>(
5670
+ export function isNumber<TForm = unknown, TField extends number | null | undefined = number>(
5251
5671
  options?: ValidateOptions
5252
5672
  ): Validator<TForm, TField>
5253
5673
  ```
@@ -5283,6 +5703,9 @@ _Source: src/core/validation/validators/is-number.ts_
5283
5703
  bulk-сеттеры (`model.set`/`model.patch`, `patchValue`/`setValue`) пропускают это поле, чтобы
5284
5704
  значение из payload не затирало вычисляемое. Прикладной код напрямую обычно не вызывает.
5285
5705
 
5706
+ Идемпотентна на уровне флага: повторные вызовы наращивают счётчик ссылок, чтобы каждый оператор,
5707
+ пишущий в этот сигнал, был сбалансирован своим {@link unmarkDerived} при dispose.
5708
+
5286
5709
  **Signature:**
5287
5710
  ```typescript
5288
5711
  export function markDerived(signal: Signal<any>): void
@@ -5302,6 +5725,7 @@ markDerived(model.$.total);
5302
5725
 
5303
5726
  **See also:**
5304
5727
  - {@link isDerived} - проверка пометки
5728
+ - {@link unmarkDerived} - снять пометку при dispose оператора
5305
5729
 
5306
5730
  _Source: src/core/utils/derived-registry.ts_
5307
5731
 
@@ -5315,7 +5739,7 @@ _Source: src/core/utils/derived-registry.ts_
5315
5739
 
5316
5740
  **Signature:**
5317
5741
  ```typescript
5318
- export function max<TForm = unknown, TField extends number | undefined = number>(
5742
+ export function max<TForm = unknown, TField extends number | null | undefined = number>(
5319
5743
  maxValue: number,
5320
5744
  options?: ValidateOptions
5321
5745
  ): Validator<TForm, TField>
@@ -5477,7 +5901,7 @@ _Source: src/core/validation/validators/max-length.ts_
5477
5901
 
5478
5902
  **Signature:**
5479
5903
  ```typescript
5480
- export function min<TForm = unknown, TField extends number | undefined = number>(
5904
+ export function min<TForm = unknown, TField extends number | null | undefined = number>(
5481
5905
  minValue: number,
5482
5906
  options?: ValidateOptions
5483
5907
  ): Validator<TForm, TField>
@@ -5645,9 +6069,17 @@ export interface ModelApi<T> {
5645
6069
  readonly $: ModelSignals<T>;
5646
6070
  /** Снимок значений (без подписки) — для submit. */
5647
6071
  get(): T;
5648
- /** Массовая замена значений (load с сервера). Не меняет initial-снимок. */
6072
+ /**
6073
+ * @deprecated Алиас {@link ModelApi.patch}: `set` выполняет то же частичное **слияние** —
6074
+ * обновляет только переданные ключи, отсутствующие НЕ трогает (это НЕ полная замена, вопреки
6075
+ * старому названию). Используйте `patch`; для замены с обнулением лишних полей делайте
6076
+ * `patch({ ...defaults, ...dto })` явно. Не меняет initial-снимок.
6077
+ */
5649
6078
  set(value: Partial<T>): void;
5650
- /** Частичное слияние значений. */
6079
+ /**
6080
+ * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
6081
+ * отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
6082
+ */
5651
6083
  patch(value: Partial<T>): void;
5652
6084
  /** Отличаются ли текущие значения от initial-снимка (value-diff). */
5653
6085
  isDirty(): boolean;
@@ -5672,6 +6104,11 @@ _Source: src/core/model/types.ts_
5672
6104
  **Signature:**
5673
6105
  ```typescript
5674
6106
  export interface ModelArray<U> {
6107
+ /**
6108
+ * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
6109
+ * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
6110
+ */
6111
+ readonly __path: string;
5675
6112
  /** Реактивная длина. */
5676
6113
  readonly length: number;
5677
6114
  /** Добавить элемент в конец (значение элемента целиком). */
@@ -5686,12 +6123,14 @@ export interface ModelArray<U> {
5686
6123
  swap(a: number, b: number): void;
5687
6124
  /** Очистить массив. */
5688
6125
  clear(): void;
5689
- /** Под-модель элемента по индексу (для объектных элементов) или значение (для примитивных). */
5690
- at(index: number): U extends object ? FormModel<U> : U | undefined;
5691
- /** Map по элементам: объектные → {@link FormModel}, примитивные → значение. */
5692
- map<R>(fn: (item: U extends object ? FormModel<U> : U, index: number) => R): R[];
5693
- /** Итерация по элементам. */
5694
- forEach(fn: (item: U extends object ? FormModel<U> : U, index: number) => void): void;
6126
+ /** Элемент по индексу: объект под-модель, массив {@link ModelArray}, лист → значение. */
6127
+ at(
6128
+ index: number
6129
+ ): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
6130
+ /** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
6131
+ map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
6132
+ /** Итерация по элементам (см. {@link ModelArrayItem}). */
6133
+ forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
5695
6134
  /** Снимок массива значений (без подписки). */
5696
6135
  toArray(): U[];
5697
6136
  /** Индексный value-доступ. */
@@ -5798,13 +6237,7 @@ _Source: src/core/model/types.ts_
5798
6237
  **Signature:**
5799
6238
  ```typescript
5800
6239
  export type ModelSignals<T> = {
5801
- [K in keyof T]: NonNullable<T[K]> extends ReadonlyArray<infer U>
5802
- ? { readonly length: number; readonly [index: number]: ModelSignals<U & object> }
5803
- : NonNullable<T[K]> extends Opaque
5804
- ? PathAwareSignal<T[K]>
5805
- : NonNullable<T[K]> extends object
5806
- ? ModelSignals<NonNullable<T[K]>>
5807
- : PathAwareSignal<T[K]>;
6240
+ [K in keyof T]: ModelSignalNode<T[K]>;
5808
6241
  };
5809
6242
  ```
5810
6243
 
@@ -5831,9 +6264,12 @@ _Source: src/core/model/validate-model.ts_
5831
6264
 
5832
6265
  **Kind:** `type`
5833
6266
 
5834
- Валидатор слоя данных. `value` — значение поля; `model` — ближайший scope (под-модель элемента
6267
+ Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
5835
6268
  массива или корень); `root` — корневая модель.
5836
6269
 
6270
+ Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
6271
+ `FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
6272
+
5837
6273
  **Signature:**
5838
6274
  ```typescript
5839
6275
  export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
@@ -5879,7 +6315,7 @@ _Source: src/core/model/types.ts_
5879
6315
 
5880
6316
  **Signature:**
5881
6317
  ```typescript
5882
- export function multipleOf<TForm = unknown, TField extends number | undefined = number>(
6318
+ export function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(
5883
6319
  divisor: number,
5884
6320
  options?: ValidateOptions
5885
6321
  ): Validator<TForm, TField>
@@ -5988,7 +6424,7 @@ _Source: src/core/factories/node-factory.ts_
5988
6424
 
5989
6425
  **Signature:**
5990
6426
  ```typescript
5991
- export function nonNegative<TForm = unknown, TField extends number | undefined = number>(
6427
+ export function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(
5992
6428
  options?: ValidateOptions
5993
6429
  ): Validator<TForm, TField>
5994
6430
  ```
@@ -6024,7 +6460,7 @@ _Source: src/core/validation/validators/non-negative.ts_
6024
6460
 
6025
6461
  **Signature:**
6026
6462
  ```typescript
6027
- export function nonZero<TForm = unknown, TField extends number | undefined = number>(
6463
+ export function nonZero<TForm = unknown, TField extends number | null | undefined = number>(
6028
6464
  options?: ValidateOptions
6029
6465
  ): Validator<TForm, TField>
6030
6466
  ```
@@ -6250,7 +6686,8 @@ _Source: src/core/utils/signal-node-registry.ts_
6250
6686
 
6251
6687
  Возвращает чистую функцию-валидатор `(value, control, root)`. Передаётся в `validate()`.
6252
6688
 
6253
- Пустыми считаются: `null`, `undefined`, `''` (пустая строка).
6689
+ Пустыми считаются: `null`, `undefined`, `''` (пустая строка), `[]` (пустой массив —
6690
+ обязательный multi-select / FormArray без выбранных элементов).
6254
6691
  Для boolean полей требуется значение `true`.
6255
6692
 
6256
6693
  **Signature:**
@@ -6350,26 +6787,31 @@ _Source: src/core/model/behaviors.ts_
6350
6787
 
6351
6788
  **Kind:** `function`
6352
6789
 
6353
- Выполняет функцию вне контекста effect
6790
+ Выполняет функцию вне контекста effect (отложенная запись на микротаск).
6354
6791
 
6355
- Более простой вариант для одиночного вызова
6792
+ Сбои fn (синхронный throw и async-rejection) маршрутизируются через {@link FormErrorHandler}, а не
6793
+ всплывают неперехваченными. Возвращает отменитель: вызов до срабатывания микротаска отменяет запись
6794
+ (liveness-охрана — не писать против снесённого узла при dispose в том же тике).
6356
6795
 
6357
6796
  **Signature:**
6358
6797
  ```typescript
6359
- export function runOutsideEffect(fn: () => void | Promise<void>): void
6798
+ export function runOutsideEffect(fn: () => void | Promise<void>): () => void
6360
6799
  ```
6361
6800
 
6362
6801
  **Parameters:**
6363
6802
  - `fn` — - Функция для выполнения
6364
6803
 
6804
+ **Returns:** Отменитель ещё не выполненной записи (no-op, если запись уже выполнена)
6805
+
6365
6806
  **Examples:**
6366
6807
 
6367
6808
  ```typescript
6368
6809
  effect(() => {
6369
6810
  const value = signal.value;
6370
- runOutsideEffect(() => {
6811
+ const cancel = runOutsideEffect(() => {
6371
6812
  otherSignal.value = transform(value);
6372
6813
  });
6814
+ onDispose(cancel); // отменить ожидающую запись, если узел снесут в этом же тике
6373
6815
  });
6374
6816
  ```
6375
6817
 
@@ -6381,8 +6823,8 @@ _Source: src/core/utils/safe-effect.ts_
6381
6823
 
6382
6824
  Создает callback, который выполняется вне контекста effect
6383
6825
 
6384
- Использует queueMicrotask для отложенного выполнения,
6385
- что позволяет модифицировать сигналы без вызова "Cycle detected"
6826
+ Откладывает вызов через {@link runOutsideEffect} — то есть получает маршрутизацию ошибок и
6827
+ обработку async-rejection «бесплатно».
6386
6828
 
6387
6829
  **Signature:**
6388
6830
  ```typescript
@@ -6420,7 +6862,9 @@ _Source: src/core/utils/safe-effect.ts_
6420
6862
 
6421
6863
  Создает версию callback с поддержкой debounce, безопасную для effect
6422
6864
 
6423
- Комбинирует debounce логику с выходом из effect контекста
6865
+ `withDebounce` уже откладывает вызов (таймер) — этого достаточно для выхода из effect-контекста,
6866
+ поэтому дополнительного `queueMicrotask` нет (устранён двойной defer). Сбои callback (в т.ч.
6867
+ async-rejection) маршрутизируются через {@link FormErrorHandler}.
6424
6868
 
6425
6869
  **Signature:**
6426
6870
  ```typescript
@@ -6450,6 +6894,48 @@ return effect(() => {
6450
6894
 
6451
6895
  _Source: src/core/utils/safe-effect.ts_
6452
6896
 
6897
+ ### SchemaArrayControl
6898
+
6899
+ **Kind:** `interface`
6900
+
6901
+ Минимальный контракт реактивного массива модели ({@link FormSchemaNode.array}).
6902
+ Совпадает по форме с рантайм-фасадом `model.<array>` (см. `ModelArray`); рендерерский
6903
+ `RenderModelArrayControl` — его расширение (добавляет `move`).
6904
+
6905
+ **Signature:**
6906
+ ```typescript
6907
+ export interface SchemaArrayControl {
6908
+ /** Путь массива в модели (dot-нотация) — нужен для резолва узла массива. */
6909
+ readonly __path: string;
6910
+ /** Реактивная длина. */
6911
+ readonly length: number;
6912
+ at(index: number): unknown;
6913
+ push(item: unknown): void;
6914
+ removeAt(index: number): void;
6915
+ }
6916
+ ```
6917
+
6918
+ _Source: src/core/types/schema-node.ts_
6919
+
6920
+ ### SchemaValidator
6921
+
6922
+ **Kind:** `type`
6923
+
6924
+ Валидатор поля схемы. Узел хранит validators гетерогенно: движок `validate*` вызывает их как
6925
+ `(value, scope, root)`, но сюда кладут любой контракт — `ValidatorFn` (value),
6926
+ `Validator` (value, control, root) или `ModelValidator` (value, model, root). Поэтому тип
6927
+ намеренно широкий по параметрам.
6928
+
6929
+ **Signature:**
6930
+ ```typescript
6931
+ export type SchemaValidator = (
6932
+ value: any,
6933
+ ...rest: any[]
6934
+ ) => ValidationError | null | Promise<ValidationError | null>;
6935
+ ```
6936
+
6937
+ _Source: src/core/types/schema-node.ts_
6938
+
6453
6939
  ### SetValueOptions
6454
6940
 
6455
6941
  **Kind:** `interface`
@@ -6534,7 +7020,7 @@ _Source: src/core/utils/form-submitter.ts_
6534
7020
 
6535
7021
  **Signature:**
6536
7022
  ```typescript
6537
- export interface SubmittableForm<T extends FormFields> {
7023
+ export interface SubmittableForm<T extends object> {
6538
7024
  /** Пометить все поля как touched */
6539
7025
  markAsTouched(): void;
6540
7026
  /** Валидировать форму */
@@ -6700,11 +7186,47 @@ _Source: src/core/types/index.ts_
6700
7186
 
6701
7187
  **Signature:**
6702
7188
  ```typescript
6703
- export type UnknownRecord = Record<string, any>;
7189
+ export type UnknownRecord = Record<string, unknown>;
6704
7190
  ```
6705
7191
 
6706
7192
  _Source: src/core/types/index.ts_
6707
7193
 
7194
+ ### unmarkDerived
7195
+
7196
+ **Kind:** `function`
7197
+
7198
+ Снять пометку производного сигнала (обратная операция к {@link markDerived}).
7199
+
7200
+ Вызывается из очистки (`onDispose`) оператора `compute`/`computeFrom`, когда поведение снимается,
7201
+ но модель/сигнал продолжают жить (динамическая перекоммутация: удалить под-схему → сохранить модель
7202
+ → bulk-set). Без этого сигнал остаётся помеченным навсегда, и bulk-сеттеры навсегда пропускают
7203
+ поле, которое больше ничем не вычисляется.
7204
+
7205
+ Учитывает счётчик ссылок: пометка снимается только когда снят последний владелец. Вызов на
7206
+ непомеченном сигнале — безопасный no-op.
7207
+
7208
+ **Signature:**
7209
+ ```typescript
7210
+ export function unmarkDerived(signal: Signal<any>): void
7211
+ ```
7212
+
7213
+ **Parameters:**
7214
+ - `signal` — - Сигнал значения из {@link FormModel}, ранее помеченный {@link markDerived}
7215
+
7216
+ **Examples:**
7217
+
7218
+ ```typescript
7219
+ import { markDerived, unmarkDerived } from '@reformer/core';
7220
+
7221
+ markDerived(model.$.total);
7222
+ onDispose(() => unmarkDerived(model.$.total)); // при снятии compute снова разрешаем bulk-set
7223
+ ```
7224
+
7225
+ **See also:**
7226
+ - {@link markDerived} - пометить сигнал производным
7227
+
7228
+ _Source: src/core/utils/derived-registry.ts_
7229
+
6708
7230
  ### url
6709
7231
 
6710
7232
  **Kind:** `function`
@@ -6761,7 +7283,7 @@ React-хук для подписки только на длину массива
6761
7283
 
6762
7284
  **Signature:**
6763
7285
  ```typescript
6764
- export function useArrayLength<T extends FormFields>(control: ArrayNode<T>): number
7286
+ export function useArrayLength<T extends object>(control: ArrayNode<T>): number
6765
7287
  ```
6766
7288
 
6767
7289
  **Parameters:**
@@ -6815,8 +7337,8 @@ React-хук для подписки на состояние формы (FieldNo
6815
7337
  **Signature:**
6816
7338
  ```typescript
6817
7339
  export function useFormControl(
6818
- control: FieldNode<FormValue> | ArrayNode<FormFields> | undefined
6819
- ): FieldControlState<FormValue> | ArrayControlState<FormFields>
7340
+ control: FieldNode<FormValue> | ArrayNode<object> | undefined
7341
+ ): FieldControlState<FormValue> | ArrayControlState<object>
6820
7342
  ```
6821
7343
 
6822
7344
  **Parameters:**
@@ -7206,16 +7728,18 @@ _Source: src/hooks/useFormControlValue.ts_
7206
7728
 
7207
7729
  **Kind:** `interface`
7208
7730
 
7209
- Опции для функции validateAsync.
7210
-
7211
7731
  **Signature:**
7212
7732
  ```typescript
7213
7733
  export interface ValidateAsyncOptions extends ValidateOptions {
7214
- /** Задержка перед выполнением валидации (в мс) */
7734
+ /** Задержка перед выполнением валидации (в мс). */
7215
7735
  debounce?: number;
7216
7736
  }
7217
7737
  ```
7218
7738
 
7739
+ **Deprecated:** Осиротевший остаток удалённого оператора `validateAsync` (Ф7): опция `debounce`
7740
+ подключалась тем оператором, которого больше нет. Рантаймом не потребляется; экспортируется
7741
+ только ради обратной совместимости.
7742
+
7219
7743
  _Source: src/core/types/validation-schema.ts_
7220
7744
 
7221
7745
  ### validateFormModel
@@ -7230,7 +7754,7 @@ In-form валидация: прогоняет {@link validateModel} и роут
7230
7754
  ```typescript
7231
7755
  export async function validateFormModel<T>(
7232
7756
  model: FormModel<T>,
7233
- schema: unknown
7757
+ schema: FormSchemaNode
7234
7758
  ): Promise<ModelValidationResult>
7235
7759
  ```
7236
7760
 
@@ -7262,7 +7786,7 @@ Headless-валидация данных (sync + async). Работает без
7262
7786
  ```typescript
7263
7787
  export async function validateModel<T>(
7264
7788
  model: FormModel<T>,
7265
- schema: unknown
7789
+ schema: FormSchemaNode
7266
7790
  ): Promise<ModelValidationResult>
7267
7791
  ```
7268
7792
 
@@ -7283,7 +7807,10 @@ _Source: src/core/model/validate-model.ts_
7283
7807
 
7284
7808
  **Signature:**
7285
7809
  ```typescript
7286
- export function validateModelSync<T>(model: FormModel<T>, schema: unknown): ModelValidationResult
7810
+ export function validateModelSync<T>(
7811
+ model: FormModel<T>,
7812
+ schema: FormSchemaNode
7813
+ ): ModelValidationResult
7287
7814
  ```
7288
7815
 
7289
7816
  **Parameters:**
@@ -7307,15 +7834,17 @@ _Source: src/core/model/validate-model.ts_
7307
7834
 
7308
7835
  **Kind:** `interface`
7309
7836
 
7310
- Опции для функции validate.
7837
+ Опции валидатора-фабрики (`required()`/`pattern()`/…). Передаются вторым (или последним)
7838
+ аргументом в фабрику и попадают в возвращаемую {@link ValidationError}.
7311
7839
 
7312
7840
  **Signature:**
7313
7841
  ```typescript
7314
7842
  export interface ValidateOptions {
7315
- /** Сообщение об ошибке */
7843
+ /** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
7844
+ * резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */
7316
7845
  message?: string;
7317
- /** Параметры ошибки */
7318
- params?: FormFields;
7846
+ /** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
7847
+ params?: Record<string, FormValue>;
7319
7848
  }
7320
7849
  ```
7321
7850
 
@@ -7332,7 +7861,7 @@ _Source: src/core/types/validation-schema.ts_
7332
7861
  export interface ValidationError {
7333
7862
  code: string;
7334
7863
  message: string;
7335
- params?: FormFields;
7864
+ params?: Record<string, FormValue>;
7336
7865
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
7337
7866
  severity?: 'error' | 'warning';
7338
7867
  }
@@ -7346,15 +7875,25 @@ _Source: src/core/types/index.ts_
7346
7875
 
7347
7876
  Чистый синхронный валидатор поля.
7348
7877
 
7349
- Принимает значение поля, прокси текущего поля (control) и прокси корня формы (root).
7350
- Возвращает ValidationError либо null. Не знает про реестр валидации.
7878
+ Сигнатура зеркалит то, что реально вызывает движок M1
7879
+ (`validateModel`/`validateFormModel`/`validateModelSync`): `validator(value, scope, root)`.
7880
+ - `value` — значение поля (`TField`);
7881
+ - `scope` — ближайшая scope-**модель** (под-модель элемента массива или корень). Из `TForm`/`TField`
7882
+ её тип не выводится, поэтому `unknown` — потребитель сужает сам (для типизированного scope
7883
+ используйте {@link ModelValidator}`<TField, TScope, TForm>`);
7884
+ - `root` — корневая модель формы `FormModel<TForm>` (реактивный value-proxy: `root.field` читает
7885
+ значение). Возвращает `ValidationError` либо `null`. Не знает про реестр валидации.
7886
+
7887
+ Совместим с полем `validators` узла схемы (см. `SchemaValidator`) — там же лежат `ModelValidator`
7888
+ и `ValidatorFn`. Встроенные фабрики (`required()`/`email()`/…) возвращают `(value) => …` и
7889
+ дополнительные аргументы игнорируют.
7351
7890
 
7352
7891
  **Signature:**
7353
7892
  ```typescript
7354
7893
  export type Validator<TForm, TField> = (
7355
7894
  value: TField,
7356
- control: FormProxy<TField>,
7357
- root: FormProxy<TForm>
7895
+ scope: unknown,
7896
+ root: FormModel<TForm>
7358
7897
  ) => ValidationError | null;
7359
7898
  ```
7360
7899
 
@@ -7362,8 +7901,9 @@ export type Validator<TForm, TField> = (
7362
7901
 
7363
7902
  Кастомный валидатор в массиве `validators` поля схемы
7364
7903
  ```typescript
7365
- const isAdult: Validator<MyForm, number> = (value, control, root) => {
7904
+ const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
7366
7905
  if (value < 18) return { code: 'tooYoung', message: '18+' };
7906
+ // root — FormModel<MyForm>: root.someOtherField читается как значение
7367
7907
  return null;
7368
7908
  };
7369
7909