@reformer/core 8.0.0 → 9.0.0-beta.1

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 (40) hide show
  1. package/dist/core/factories/node-factory.d.ts +5 -4
  2. package/dist/core/model/types.d.ts +41 -12
  3. package/dist/core/model/validate-model.d.ts +8 -4
  4. package/dist/core/nodes/field-node.d.ts +2 -2
  5. package/dist/core/nodes/group-node.d.ts +7 -1
  6. package/dist/core/nodes/model-array-node.d.ts +5 -0
  7. package/dist/core/types/deep-schema.d.ts +16 -4
  8. package/dist/core/types/form-proxy.d.ts +1 -1
  9. package/dist/core/types/index.d.ts +4 -10
  10. package/dist/core/types/schema-node.d.ts +79 -0
  11. package/dist/core/types/validation-schema.d.ts +22 -11
  12. package/dist/core/utils/create-form.d.ts +4 -3
  13. package/dist/core/utils/error-handler.d.ts +4 -1
  14. package/dist/core/utils/form-submitter.d.ts +2 -3
  15. package/dist/core/utils/type-guards.d.ts +3 -3
  16. package/dist/core/validation/validators/date-utils.d.ts +5 -1
  17. package/dist/core/validation/validators/integer.d.ts +1 -1
  18. package/dist/core/validation/validators/is-number.d.ts +1 -1
  19. package/dist/core/validation/validators/max.d.ts +1 -1
  20. package/dist/core/validation/validators/min.d.ts +1 -1
  21. package/dist/core/validation/validators/multiple-of.d.ts +1 -1
  22. package/dist/core/validation/validators/non-negative.d.ts +1 -1
  23. package/dist/core/validation/validators/non-zero.d.ts +1 -1
  24. package/dist/{date-utils-xUWFslTj.js → date-utils-dTT_x_Be.js} +12 -10
  25. package/dist/hooks/types.d.ts +1 -1
  26. package/dist/hooks/useArrayLength.d.ts +1 -2
  27. package/dist/hooks/useFormControl.d.ts +2 -2
  28. package/dist/index.js +514 -514
  29. package/dist/signals.d.ts +13 -0
  30. package/dist/signals.js +9 -0
  31. package/dist/validators/future-date.js +1 -1
  32. package/dist/validators/is-date.js +1 -1
  33. package/dist/validators/max-age.js +1 -1
  34. package/dist/validators/max-date.js +1 -1
  35. package/dist/validators/min-age.js +1 -1
  36. package/dist/validators/min-date.js +1 -1
  37. package/dist/validators/multiple-of.js +11 -7
  38. package/dist/validators/past-date.js +1 -1
  39. package/llms.txt +568 -155
  40. package/package.json +6 -2
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`.
1529
+
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`).
1429
1534
 
1430
- ## 16. 14.5 UI COMPONENT PATTERNS
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
  }
@@ -3614,16 +3912,16 @@ _Source: src/core/types/index.ts_
3614
3912
 
3615
3913
  **Kind:** `type`
3616
3914
 
3617
- Чистый асинхронный валидатор поля.
3618
-
3619
- Регистрируется через `validateAsync(path, validator, { debounce })`.
3915
+ Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
3916
+ {@link Validator}, но возвращает `Promise`. Регистрируется через
3917
+ `validateAsync(path, validator, { debounce })`.
3620
3918
 
3621
3919
  **Signature:**
3622
3920
  ```typescript
3623
3921
  export type AsyncValidator<TForm, TField> = (
3624
3922
  value: TField,
3625
- control: FormProxy<TField>,
3626
- root: FormProxy<TForm>
3923
+ scope: unknown,
3924
+ root: FormModel<TForm>
3627
3925
  ) => Promise<ValidationError | null>;
3628
3926
  ```
3629
3927
 
@@ -3925,10 +4223,10 @@ export interface CreateFormFromModelArgs<T> {
3925
4223
  /** Реактивная модель данных (источник истины значений). */
3926
4224
  model: FormModel<T>;
3927
4225
  /**
3928
- * Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг поля к ноде
3929
- * по идентичности сигнала (`node.value === model.$.path`). Опциональна.
4226
+ * Единая Schema (дерево узлов {@link FormSchemaNode}). createForm обходит её и привязывает конфиг
4227
+ * поля к ноде по идентичности сигнала (`node.value === model.$.path`). Опциональна.
3930
4228
  */
3931
- schema?: unknown;
4229
+ schema?: FormSchemaNode;
3932
4230
  /**
3933
4231
  * Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
3934
4232
  * заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
@@ -4081,7 +4379,7 @@ export interface ErrorFilterOptions {
4081
4379
  message?: string;
4082
4380
 
4083
4381
  /** Фильтр по параметрам ошибки */
4084
- params?: FormFields;
4382
+ params?: Record<string, FormValue>;
4085
4383
 
4086
4384
  /** Кастомный предикат для фильтрации */
4087
4385
  predicate?: (error: ValidationError) => boolean;
@@ -4334,8 +4632,7 @@ export interface FieldControlState<T> {
4334
4632
  * );
4335
4633
  * ```
4336
4634
  */
4337
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4338
- componentProps: Record<string, any>;
4635
+ componentProps: Record<string, unknown>;
4339
4636
  }
4340
4637
  ```
4341
4638
 
@@ -4599,22 +4896,6 @@ try {
4599
4896
 
4600
4897
  _Source: src/core/utils/error-handler.ts_
4601
4898
 
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
4899
  ### FormModel
4619
4900
 
4620
4901
  **Kind:** `type`
@@ -4711,12 +4992,17 @@ _Source: src/core/types/form-proxy.ts_
4711
4992
 
4712
4993
  **Kind:** `type`
4713
4994
 
4714
- Автоматически определяет тип схемы на основе TypeScript типа:
4995
+ **Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
4996
+ (или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
4715
4997
  - `T[] -> [FormSchema<T>]` (массив с одним элементом)
4716
4998
  - `object -> FormSchema<T>` (группа)
4717
4999
  - `primitive -> FieldConfig<T>` (поле)
4718
5000
 
4719
- Использует NonNullable для корректной обработки опциональных полей
5001
+ Использует NonNullable для корректной обработки опциональных полей.
5002
+
5003
+ ⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
5004
+ контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
5005
+ (ключи = поля данных), а не узел дерева.
4720
5006
 
4721
5007
  **Signature:**
4722
5008
  ```typescript
@@ -4765,6 +5051,65 @@ const schema: FormSchema<Form> = {
4765
5051
 
4766
5052
  _Source: src/core/types/deep-schema.ts_
4767
5053
 
5054
+ ### FormSchemaNode
5055
+
5056
+ **Kind:** `interface`
5057
+
5058
+ Узел единой схемы M1 — дерево, обходимое `createForm({ model, schema })`,
5059
+ `validateModel`/`validateFormModel` и рендерерами.
5060
+
5061
+ Узел совмещает несколько ролей (различаются рантаймом по форме):
5062
+ - **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
5063
+ - **массив** — `{ array: model.<path>, item(itemModel) }`;
5064
+ - **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
5065
+ - **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).
5066
+
5067
+ Индексная сигнатура (`[key: string]: unknown`) отражает свободный рекурсивный обход: под-узлы
5068
+ допустимы под любым ключом. Известные поля типизированы (даёт автокомплит и проверку их типов).
5069
+
5070
+ **Signature:**
5071
+ ```typescript
5072
+ export interface FormSchemaNode {
5073
+ /**
5074
+ * «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма
5075
+ * зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до
5076
+ * `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
5077
+ */
5078
+ value?: unknown;
5079
+ /** UI-компонент. Опционален: core-часть работает без UI (значение/валидация). */
5080
+ component?: ComponentType<any>;
5081
+ /** Props компонента. Также «клапан» для вложенности под-узлов (напр. steps визарда). */
5082
+ componentProps?: Record<string, unknown>;
5083
+ validators?: SchemaValidator[];
5084
+ asyncValidators?: SchemaValidator[];
5085
+ updateOn?: 'change' | 'blur' | 'submit';
5086
+ disabled?: boolean;
5087
+ /** Задержка (мс) перед запуском асинхронной валидации. */
5088
+ debounce?: number;
5089
+ /** Идентификатор узла (для wizard/tabs/renderBehavior). */
5090
+ selector?: string;
5091
+ testId?: string;
5092
+ /** Дочерние узлы (даёт контекстную типизацию вложенным литералам — value/validators/when). */
5093
+ children?: readonly FormSchemaNode[];
5094
+ /** Условие включения поддерева (branch-узел `{ when, children }`). */
5095
+ when?: (scope: any, root: any) => boolean;
5096
+ /** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */
5097
+ array?: SchemaArrayControl;
5098
+ /** Схема элемента массива: под-модель элемента → узел поддерева. */
5099
+ item?: (itemModel: any) => FormSchemaNode;
5100
+ /**
5101
+ * Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,
5102
+ * либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`
5103
+ * схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.
5104
+ */
5105
+ initialValue?: unknown;
5106
+ /** Свободная вложенность: record-of-fields и произвольные под-узлы. */
5107
+ [key: string]: unknown;
5108
+ }
5109
+ ```
5110
+
5111
+ _Source: src/core/types/schema-node.ts_
5112
+
4768
5113
  ### FormStatusMachine
4769
5114
 
4770
5115
  **Kind:** `class`
@@ -4810,7 +5155,7 @@ FormSubmitter - управляет процессом отправки форм
4810
5155
 
4811
5156
  **Signature:**
4812
5157
  ```typescript
4813
- export class FormSubmitter<T extends FormFields> {
5158
+ export class FormSubmitter<T extends object> {
4814
5159
  /** Внутренний сигнал состояния отправки */ /* … */ }
4815
5160
  ```
4816
5161
 
@@ -5018,7 +5363,7 @@ _Source: src/core/types/index.ts_
5018
5363
 
5019
5364
  **Signature:**
5020
5365
  ```typescript
5021
- export function integer<TForm = unknown, TField extends number | undefined = number>(
5366
+ export function integer<TForm = unknown, TField extends number | null | undefined = number>(
5022
5367
  options?: ValidateOptions
5023
5368
  ): Validator<TForm, TField>
5024
5369
  ```
@@ -5055,7 +5400,7 @@ ArrayNode представляет массив вложенных форм (о
5055
5400
 
5056
5401
  **Signature:**
5057
5402
  ```typescript
5058
- export function isArrayNode(value: unknown): value is ArrayNode<FormFields>
5403
+ export function isArrayNode(value: unknown): value is ArrayNode<object>
5059
5404
  ```
5060
5405
 
5061
5406
  **Parameters:**
@@ -5217,7 +5562,7 @@ GroupNode представляет объект с вложенными поля
5217
5562
 
5218
5563
  **Signature:**
5219
5564
  ```typescript
5220
- export function isGroupNode(value: unknown): value is GroupNode<FormFields>
5565
+ export function isGroupNode(value: unknown): value is GroupNode<object>
5221
5566
  ```
5222
5567
 
5223
5568
  **Parameters:**
@@ -5247,7 +5592,7 @@ _Source: src/core/utils/type-guards.ts_
5247
5592
 
5248
5593
  **Signature:**
5249
5594
  ```typescript
5250
- export function isNumber<TForm = unknown, TField extends number | undefined = number>(
5595
+ export function isNumber<TForm = unknown, TField extends number | null | undefined = number>(
5251
5596
  options?: ValidateOptions
5252
5597
  ): Validator<TForm, TField>
5253
5598
  ```
@@ -5315,7 +5660,7 @@ _Source: src/core/utils/derived-registry.ts_
5315
5660
 
5316
5661
  **Signature:**
5317
5662
  ```typescript
5318
- export function max<TForm = unknown, TField extends number | undefined = number>(
5663
+ export function max<TForm = unknown, TField extends number | null | undefined = number>(
5319
5664
  maxValue: number,
5320
5665
  options?: ValidateOptions
5321
5666
  ): Validator<TForm, TField>
@@ -5477,7 +5822,7 @@ _Source: src/core/validation/validators/max-length.ts_
5477
5822
 
5478
5823
  **Signature:**
5479
5824
  ```typescript
5480
- export function min<TForm = unknown, TField extends number | undefined = number>(
5825
+ export function min<TForm = unknown, TField extends number | null | undefined = number>(
5481
5826
  minValue: number,
5482
5827
  options?: ValidateOptions
5483
5828
  ): Validator<TForm, TField>
@@ -5645,9 +5990,17 @@ export interface ModelApi<T> {
5645
5990
  readonly $: ModelSignals<T>;
5646
5991
  /** Снимок значений (без подписки) — для submit. */
5647
5992
  get(): T;
5648
- /** Массовая замена значений (load с сервера). Не меняет initial-снимок. */
5993
+ /**
5994
+ * @deprecated Алиас {@link ModelApi.patch}: `set` выполняет то же частичное **слияние** —
5995
+ * обновляет только переданные ключи, отсутствующие НЕ трогает (это НЕ полная замена, вопреки
5996
+ * старому названию). Используйте `patch`; для замены с обнулением лишних полей делайте
5997
+ * `patch({ ...defaults, ...dto })` явно. Не меняет initial-снимок.
5998
+ */
5649
5999
  set(value: Partial<T>): void;
5650
- /** Частичное слияние значений. */
6000
+ /**
6001
+ * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
6002
+ * отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
6003
+ */
5651
6004
  patch(value: Partial<T>): void;
5652
6005
  /** Отличаются ли текущие значения от initial-снимка (value-diff). */
5653
6006
  isDirty(): boolean;
@@ -5672,6 +6025,11 @@ _Source: src/core/model/types.ts_
5672
6025
  **Signature:**
5673
6026
  ```typescript
5674
6027
  export interface ModelArray<U> {
6028
+ /**
6029
+ * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
6030
+ * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
6031
+ */
6032
+ readonly __path: string;
5675
6033
  /** Реактивная длина. */
5676
6034
  readonly length: number;
5677
6035
  /** Добавить элемент в конец (значение элемента целиком). */
@@ -5686,12 +6044,14 @@ export interface ModelArray<U> {
5686
6044
  swap(a: number, b: number): void;
5687
6045
  /** Очистить массив. */
5688
6046
  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;
6047
+ /** Элемент по индексу: объект под-модель, массив {@link ModelArray}, лист → значение. */
6048
+ at(
6049
+ index: number
6050
+ ): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
6051
+ /** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
6052
+ map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
6053
+ /** Итерация по элементам (см. {@link ModelArrayItem}). */
6054
+ forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
5695
6055
  /** Снимок массива значений (без подписки). */
5696
6056
  toArray(): U[];
5697
6057
  /** Индексный value-доступ. */
@@ -5798,13 +6158,7 @@ _Source: src/core/model/types.ts_
5798
6158
  **Signature:**
5799
6159
  ```typescript
5800
6160
  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]>;
6161
+ [K in keyof T]: ModelSignalNode<T[K]>;
5808
6162
  };
5809
6163
  ```
5810
6164
 
@@ -5831,9 +6185,12 @@ _Source: src/core/model/validate-model.ts_
5831
6185
 
5832
6186
  **Kind:** `type`
5833
6187
 
5834
- Валидатор слоя данных. `value` — значение поля; `model` — ближайший scope (под-модель элемента
6188
+ Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
5835
6189
  массива или корень); `root` — корневая модель.
5836
6190
 
6191
+ Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
6192
+ `FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
6193
+
5837
6194
  **Signature:**
5838
6195
  ```typescript
5839
6196
  export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
@@ -5879,7 +6236,7 @@ _Source: src/core/model/types.ts_
5879
6236
 
5880
6237
  **Signature:**
5881
6238
  ```typescript
5882
- export function multipleOf<TForm = unknown, TField extends number | undefined = number>(
6239
+ export function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(
5883
6240
  divisor: number,
5884
6241
  options?: ValidateOptions
5885
6242
  ): Validator<TForm, TField>
@@ -5988,7 +6345,7 @@ _Source: src/core/factories/node-factory.ts_
5988
6345
 
5989
6346
  **Signature:**
5990
6347
  ```typescript
5991
- export function nonNegative<TForm = unknown, TField extends number | undefined = number>(
6348
+ export function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(
5992
6349
  options?: ValidateOptions
5993
6350
  ): Validator<TForm, TField>
5994
6351
  ```
@@ -6024,7 +6381,7 @@ _Source: src/core/validation/validators/non-negative.ts_
6024
6381
 
6025
6382
  **Signature:**
6026
6383
  ```typescript
6027
- export function nonZero<TForm = unknown, TField extends number | undefined = number>(
6384
+ export function nonZero<TForm = unknown, TField extends number | null | undefined = number>(
6028
6385
  options?: ValidateOptions
6029
6386
  ): Validator<TForm, TField>
6030
6387
  ```
@@ -6450,6 +6807,48 @@ return effect(() => {
6450
6807
 
6451
6808
  _Source: src/core/utils/safe-effect.ts_
6452
6809
 
6810
+ ### SchemaArrayControl
6811
+
6812
+ **Kind:** `interface`
6813
+
6814
+ Минимальный контракт реактивного массива модели ({@link FormSchemaNode.array}).
6815
+ Совпадает по форме с рантайм-фасадом `model.<array>` (см. `ModelArray`); рендерерский
6816
+ `RenderModelArrayControl` — его расширение (добавляет `move`).
6817
+
6818
+ **Signature:**
6819
+ ```typescript
6820
+ export interface SchemaArrayControl {
6821
+ /** Путь массива в модели (dot-нотация) — нужен для резолва узла массива. */
6822
+ readonly __path: string;
6823
+ /** Реактивная длина. */
6824
+ readonly length: number;
6825
+ at(index: number): unknown;
6826
+ push(item: unknown): void;
6827
+ removeAt(index: number): void;
6828
+ }
6829
+ ```
6830
+
6831
+ _Source: src/core/types/schema-node.ts_
6832
+
6833
+ ### SchemaValidator
6834
+
6835
+ **Kind:** `type`
6836
+
6837
+ Валидатор поля схемы. Узел хранит validators гетерогенно: движок `validate*` вызывает их как
6838
+ `(value, scope, root)`, но сюда кладут любой контракт — `ValidatorFn` (value),
6839
+ `Validator` (value, control, root) или `ModelValidator` (value, model, root). Поэтому тип
6840
+ намеренно широкий по параметрам.
6841
+
6842
+ **Signature:**
6843
+ ```typescript
6844
+ export type SchemaValidator = (
6845
+ value: any,
6846
+ ...rest: any[]
6847
+ ) => ValidationError | null | Promise<ValidationError | null>;
6848
+ ```
6849
+
6850
+ _Source: src/core/types/schema-node.ts_
6851
+
6453
6852
  ### SetValueOptions
6454
6853
 
6455
6854
  **Kind:** `interface`
@@ -6534,7 +6933,7 @@ _Source: src/core/utils/form-submitter.ts_
6534
6933
 
6535
6934
  **Signature:**
6536
6935
  ```typescript
6537
- export interface SubmittableForm<T extends FormFields> {
6936
+ export interface SubmittableForm<T extends object> {
6538
6937
  /** Пометить все поля как touched */
6539
6938
  markAsTouched(): void;
6540
6939
  /** Валидировать форму */
@@ -6700,7 +7099,7 @@ _Source: src/core/types/index.ts_
6700
7099
 
6701
7100
  **Signature:**
6702
7101
  ```typescript
6703
- export type UnknownRecord = Record<string, any>;
7102
+ export type UnknownRecord = Record<string, unknown>;
6704
7103
  ```
6705
7104
 
6706
7105
  _Source: src/core/types/index.ts_
@@ -6761,7 +7160,7 @@ React-хук для подписки только на длину массива
6761
7160
 
6762
7161
  **Signature:**
6763
7162
  ```typescript
6764
- export function useArrayLength<T extends FormFields>(control: ArrayNode<T>): number
7163
+ export function useArrayLength<T extends object>(control: ArrayNode<T>): number
6765
7164
  ```
6766
7165
 
6767
7166
  **Parameters:**
@@ -6815,8 +7214,8 @@ React-хук для подписки на состояние формы (FieldNo
6815
7214
  **Signature:**
6816
7215
  ```typescript
6817
7216
  export function useFormControl(
6818
- control: FieldNode<FormValue> | ArrayNode<FormFields> | undefined
6819
- ): FieldControlState<FormValue> | ArrayControlState<FormFields>
7217
+ control: FieldNode<FormValue> | ArrayNode<object> | undefined
7218
+ ): FieldControlState<FormValue> | ArrayControlState<object>
6820
7219
  ```
6821
7220
 
6822
7221
  **Parameters:**
@@ -7230,7 +7629,7 @@ In-form валидация: прогоняет {@link validateModel} и роут
7230
7629
  ```typescript
7231
7630
  export async function validateFormModel<T>(
7232
7631
  model: FormModel<T>,
7233
- schema: unknown
7632
+ schema: FormSchemaNode
7234
7633
  ): Promise<ModelValidationResult>
7235
7634
  ```
7236
7635
 
@@ -7262,7 +7661,7 @@ Headless-валидация данных (sync + async). Работает без
7262
7661
  ```typescript
7263
7662
  export async function validateModel<T>(
7264
7663
  model: FormModel<T>,
7265
- schema: unknown
7664
+ schema: FormSchemaNode
7266
7665
  ): Promise<ModelValidationResult>
7267
7666
  ```
7268
7667
 
@@ -7283,7 +7682,10 @@ _Source: src/core/model/validate-model.ts_
7283
7682
 
7284
7683
  **Signature:**
7285
7684
  ```typescript
7286
- export function validateModelSync<T>(model: FormModel<T>, schema: unknown): ModelValidationResult
7685
+ export function validateModelSync<T>(
7686
+ model: FormModel<T>,
7687
+ schema: FormSchemaNode
7688
+ ): ModelValidationResult
7287
7689
  ```
7288
7690
 
7289
7691
  **Parameters:**
@@ -7315,7 +7717,7 @@ export interface ValidateOptions {
7315
7717
  /** Сообщение об ошибке */
7316
7718
  message?: string;
7317
7719
  /** Параметры ошибки */
7318
- params?: FormFields;
7720
+ params?: Record<string, FormValue>;
7319
7721
  }
7320
7722
  ```
7321
7723
 
@@ -7332,7 +7734,7 @@ _Source: src/core/types/validation-schema.ts_
7332
7734
  export interface ValidationError {
7333
7735
  code: string;
7334
7736
  message: string;
7335
- params?: FormFields;
7737
+ params?: Record<string, FormValue>;
7336
7738
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
7337
7739
  severity?: 'error' | 'warning';
7338
7740
  }
@@ -7346,15 +7748,25 @@ _Source: src/core/types/index.ts_
7346
7748
 
7347
7749
  Чистый синхронный валидатор поля.
7348
7750
 
7349
- Принимает значение поля, прокси текущего поля (control) и прокси корня формы (root).
7350
- Возвращает ValidationError либо null. Не знает про реестр валидации.
7751
+ Сигнатура зеркалит то, что реально вызывает движок M1
7752
+ (`validateModel`/`validateFormModel`/`validateModelSync`): `validator(value, scope, root)`.
7753
+ - `value` — значение поля (`TField`);
7754
+ - `scope` — ближайшая scope-**модель** (под-модель элемента массива или корень). Из `TForm`/`TField`
7755
+ её тип не выводится, поэтому `unknown` — потребитель сужает сам (для типизированного scope
7756
+ используйте {@link ModelValidator}`<TField, TScope, TForm>`);
7757
+ - `root` — корневая модель формы `FormModel<TForm>` (реактивный value-proxy: `root.field` читает
7758
+ значение). Возвращает `ValidationError` либо `null`. Не знает про реестр валидации.
7759
+
7760
+ Совместим с полем `validators` узла схемы (см. `SchemaValidator`) — там же лежат `ModelValidator`
7761
+ и `ValidatorFn`. Встроенные фабрики (`required()`/`email()`/…) возвращают `(value) => …` и
7762
+ дополнительные аргументы игнорируют.
7351
7763
 
7352
7764
  **Signature:**
7353
7765
  ```typescript
7354
7766
  export type Validator<TForm, TField> = (
7355
7767
  value: TField,
7356
- control: FormProxy<TField>,
7357
- root: FormProxy<TForm>
7768
+ scope: unknown,
7769
+ root: FormModel<TForm>
7358
7770
  ) => ValidationError | null;
7359
7771
  ```
7360
7772
 
@@ -7362,8 +7774,9 @@ export type Validator<TForm, TField> = (
7362
7774
 
7363
7775
  Кастомный валидатор в массиве `validators` поля схемы
7364
7776
  ```typescript
7365
- const isAdult: Validator<MyForm, number> = (value, control, root) => {
7777
+ const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
7366
7778
  if (value < 18) return { code: 'tooYoung', message: '18+' };
7779
+ // root — FormModel<MyForm>: root.someOtherField читается как значение
7367
7780
  return null;
7368
7781
  };
7369
7782