@reformer/core 7.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 +586 -164
  40. package/package.json +7 -3
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
@@ -888,6 +889,10 @@ UI-конфиг (`component`/`componentProps`) + валидаторы. Знач
888
889
  }
889
890
  ```
890
891
 
892
+ > `disabled` — **top-level поле узла** (начальное состояние); дальше управляется методами
893
+ > `form.field.disable()`/`enable()` и оператором `enableWhen`. `componentProps.disabled` — это
894
+ > UI-проп и на состояние узла **не влияет** (частая причина «computed-поле остаётся редактируемым»).
895
+
891
896
  ### Primitive Fields
892
897
 
893
898
  ```typescript
@@ -983,6 +988,11 @@ form.address.city.value.value; // текущее значение (через
983
988
  model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели
984
989
  ```
985
990
 
991
+ > **Тип `schema` в M1 — `unknown`, не `FormSchema<T>`.** Это осознанно: интерпретатор обходит
992
+ > произвольную структуру и собирает листья `{ value: signal, component?, ... }`, поэтому обёртки
993
+ > (`{ children: [...] }`, wizard/steps) компилируются. Строгий `FormSchema<T>` (keyed-map по полям
994
+ > `T`) применяется только на legacy-пути `createForm(schema)` без модели.
995
+
986
996
  ### createForm Returns a Proxy
987
997
 
988
998
  ```typescript
@@ -1031,6 +1041,39 @@ const form = createForm<MyForm>({ model, schema });
1031
1041
  > он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
1032
1042
  > См. `30-type-safety-recipes.md`.
1033
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
+
1034
1077
  ### Array operations — на модели
1035
1078
 
1036
1079
  Мутации массива делаются через `ModelArray` (`model.items`), а не через ноду формы:
@@ -1051,6 +1094,16 @@ model.items.map((item, i) => item.name); // item — FormModel<Item>
1051
1094
  > (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
1052
1095
  > берутся из `item`-фабрики схемы автоматически.
1053
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
+
1054
1107
  ### Rendering Arrays
1055
1108
 
1056
1109
  Каждый элемент массива — под-форма (`FormProxy<Item>`). Итерируй через `form.items.map`:
@@ -1103,7 +1156,12 @@ const uniqueNames: ModelValidator<unknown, unknown, MyForm> = (_value, _scope, r
1103
1156
  };
1104
1157
  ```
1105
1158
 
1106
- ## 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)
1107
1165
 
1108
1166
  Для async-реакции на изменение поля (загрузка зависимых опций, справочников) используй
1109
1167
  `onChange` из `@reformer/core/behaviors`. Колбэк выполняется ВНЕ effect-контекста (можно
@@ -1157,7 +1215,7 @@ const stop = watchField(model.$.country, (country) => {
1157
1215
  // stop() — отписаться
1158
1216
  ```
1159
1217
 
1160
- ## 12. 11. ARRAY CLEANUP PATTERN
1218
+ ## 13. 11. ARRAY CLEANUP PATTERN
1161
1219
 
1162
1220
  Очистка массива при выключении флага — через `onChange` на сигнале флага + `clear()` на
1163
1221
  модели-массиве. Колбит выполняется вне effect-контекста, поэтому мутировать массив безопасно.
@@ -1189,7 +1247,7 @@ function clearWhenOff(flag: ReadonlySignal<boolean>, array: { clear(): void }):
1189
1247
  clearWhenOff(model.$.hasProperty, model.properties);
1190
1248
  ```
1191
1249
 
1192
- ## 13. 12. MULTI-STEP FORM VALIDATION
1250
+ ## 14. 12. MULTI-STEP FORM VALIDATION
1193
1251
 
1194
1252
  Каждый шаг — своя под-схема валидации (дерево узлов `{ children: [...] }`). Переход к
1195
1253
  следующему шагу проверяется `validateFormModel(model, stepSchema)`; полный submit — по
@@ -1266,11 +1324,7 @@ function MultiStepForm() {
1266
1324
  }
1267
1325
  ```
1268
1326
 
1269
- > В монорепо `FormWizard` из `@reformer/ui-kit` принимает колбэки `{ validateStep, validateAll }`,
1270
- > собранные фабрикой `makeCreditValidationConfig(model)` поверх `validateFormModel`. См.
1271
- > `complex-multy-step-form/schemas/validation.ts`.
1272
-
1273
- ## 14. 13. EXTENDED COMMON MISTAKES
1327
+ ## 15. 13. EXTENDED COMMON MISTAKES
1274
1328
 
1275
1329
  ### Reuse via apply / applyEach — не дублируй код
1276
1330
 
@@ -1352,30 +1406,37 @@ revalidateWhen([model.$.field2], () => validateFormModel(model, schema));
1352
1406
  > `validateGroup`/`validateTree`/`validateForm`, типы `FieldPath`/`ValidationSchemaFn`/`BehaviorSchemaFn`
1353
1407
  > и `ctx.form.*`/`ctx.setFieldValue` — из старой path-based архитектуры и УДАЛЕНЫ. См. `17-nonexistent-api.md`.
1354
1408
 
1355
- ## 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.
1356
1419
 
1357
1420
  ```
1358
1421
  src/
1359
- ├── components/ui/ # Reusable UI components
1360
- │ ├── FormField.tsx
1361
- │ └── FormArraySection.tsx
1422
+ ├── components/ui/ # App-wide reusable UI (FormField, FormArraySection, ...)
1362
1423
 
1363
1424
  ├── forms/
1364
- │ └── [form-name]/ # Form module
1365
- │ ├── types.ts # Form data type (type-alias)
1366
- │ ├── model.ts # createModel factory + initial values
1367
- │ ├── schema.ts # createForm schema (value: model.$.x)
1368
- │ ├── validation.ts # validation schema + validateFormModel config
1369
- │ ├── behavior.ts # defineFormBehavior(...)
1370
- │ ├── [FormName]Form.tsx # Main component
1371
-
1372
- ├── steps/ # Multi-step wizard
1373
- │ │ └── loan-info/
1374
- │ │ └── LoanInfoForm.tsx
1375
- │ │
1376
- │ └── 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
1377
1434
  ```
1378
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
+
1379
1440
  ### Key Files
1380
1441
 
1381
1442
  ```typescript
@@ -1391,7 +1452,7 @@ import { createModel, type FormModel } from '@reformer/core';
1391
1452
  export const createCreditApplicationModel = (): FormModel<CreditApplicationForm> =>
1392
1453
  createModel<CreditApplicationForm>(createInitialCreditApplication());
1393
1454
 
1394
- // forms/credit-application/schema.ts
1455
+ // forms/credit-application/form.schema.ts
1395
1456
  import type { FormModel } from '@reformer/core';
1396
1457
  export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
1397
1458
  loanType: { value: model.$.loanType, component: Select, componentProps: { /* ... */ } },
@@ -1399,14 +1460,14 @@ export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>)
1399
1460
  properties: { array: model.properties, item: propertyItem },
1400
1461
  });
1401
1462
 
1402
- // forms/credit-application/behavior.ts
1463
+ // forms/credit-application/form.behavior.ts
1403
1464
  import { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';
1404
1465
  export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(({ model }) => {
1405
1466
  compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
1406
1467
  enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
1407
1468
  });
1408
1469
 
1409
- // forms/credit-application/create-form.tsсборка
1470
+ // forms/credit-application/index.tsxentry: assembles the form, renders <FormWizard> with all steps inline
1410
1471
  import { createForm } from '@reformer/core';
1411
1472
  export const createCreditApplicationForm = () => {
1412
1473
  const model = createCreditApplicationModel();
@@ -1414,15 +1475,64 @@ export const createCreditApplicationForm = () => {
1414
1475
  };
1415
1476
  ```
1416
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
+
1417
1518
  ### Scaling
1418
1519
 
1419
- | Complexity | Structure |
1420
- | ---------- | ---------------------------------------------------------------------------- |
1421
- | Simple | Single file: `ContactForm.tsx` (model + schema + component) |
1422
- | Medium | Separate files: `types.ts`, `model.ts`, `schema.ts`, `validation.ts`, `Form.tsx` |
1423
- | 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`).
1424
1534
 
1425
- ## 16. 14.5 UI COMPONENT PATTERNS
1535
+ ## 17. 14.5 UI COMPONENT PATTERNS
1426
1536
 
1427
1537
  > **Default rule (read first)**: для UI используй `FormField` из
1428
1538
  > [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
@@ -1607,7 +1717,7 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
1607
1717
  }
1608
1718
  ```
1609
1719
 
1610
- ## 17. 15. NON-EXISTENT API (DO NOT USE)
1720
+ ## 18. 15. NON-EXISTENT API (DO NOT USE)
1611
1721
 
1612
1722
  **Следующего API НЕТ в @reformer/core** (частично — наследие старой path-based архитектуры,
1613
1723
  удалено при переходе на M1):
@@ -1671,7 +1781,166 @@ enableWhen(path.city, (form) => Boolean(form.country));
1671
1781
  enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
1672
1782
  ```
1673
1783
 
1674
- ## 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)
1675
1944
 
1676
1945
  Под M1 значения живут в модели. Есть три контекста чтения: value-доступ модели, сигналы, и React-хуки.
1677
1946
 
@@ -1730,7 +1999,7 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
1730
1999
  > цикла нет). Для side-эффектов (загрузка опций, обновление componentProps) — `onChange`.
1731
2000
  > `computeFrom`/`copyFrom`/`enableWhen` принимают сигналы (`model.$.x`), НЕ строковые пути.
1732
2001
 
1733
- ## 19. 17. COMPUTE vs ONCHANGE
2002
+ ## 26. 17. COMPUTE vs ONCHANGE
1734
2003
 
1735
2004
  Под M1 behaviors работают на сигналах модели. Есть два способа их писать:
1736
2005
 
@@ -1767,6 +2036,44 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
1767
2036
  });
1768
2037
  ```
1769
2038
 
2039
+ > **Не читай `model.get()` внутри `compute`/`when`** — это нереактивный снимок, зависимость не
2040
+ > отследится и пересчёта не будет. Читай поля по отдельности (`model.field` или `model.$.field.value`).
2041
+ > `model.get()` — только вне реактивного контекста (в `onChange`, обработчиках событий).
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
+
1770
2077
  ### computeFrom — явный список источников
1771
2078
 
1772
2079
  Когда нужен явный контроль зависимостей — `computeFrom(sources, target, fn)`. Значения
@@ -1831,7 +2138,7 @@ const stop = watchField(model.$.country, () => { model.city = ''; });
1831
2138
  порядке через реактивный граф (цель одного = источник другого). Расходящиеся взаимные
1832
2139
  `compute` без стабилизации бросят понятную ошибку (см. `22-cycle-detection.md`).
1833
2140
 
1834
- ## 20. 18. ARRAY OPERATIONS
2141
+ ## 27. 18. ARRAY OPERATIONS
1835
2142
 
1836
2143
  Массивы объектов — model-owned. Мутации делаются через `ModelArray` (`model.arrayField`);
1837
2144
  рендер — через ноду формы (`form.items.map` / `.at`).
@@ -1939,7 +2246,7 @@ const percentagesSumTo100: ModelValidator<unknown, unknown, MyForm> = (_v, _s, r
1939
2246
  };
1940
2247
  ```
1941
2248
 
1942
- ## 21. Как устроено под M1
2249
+ ## 28. Как устроено под M1
1943
2250
 
1944
2251
  Behaviors на сигналах уже защищены от типичных циклов:
1945
2252
 
@@ -1952,7 +2259,7 @@ Behaviors на сигналах уже защищены от типичных ц
1952
2259
  Тебе НЕ нужно вручную ставить `{ immediate: false }`, guard'ить `disabled.value` перед
1953
2260
  `disable()` или сравнивать значения перед записью — это делается внутри операторов.
1954
2261
 
1955
- ## 22. Когда всё-таки возникает «Cycle detected»
2262
+ ## 29. Когда всё-таки возникает «Cycle detected»
1956
2263
 
1957
2264
  ### 1. Расходящийся взаимный compute
1958
2265
 
@@ -1999,7 +2306,7 @@ resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
1999
2306
  resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
2000
2307
  ```
2001
2308
 
2002
- ## 23. Prefer built-in operators over manual logic
2309
+ ## 30. Prefer built-in operators over manual logic
2003
2310
 
2004
2311
  Вместо ручной сборки reset-on-disable через `onChange` — используй `enableWhen({ resetOnDisable })`:
2005
2312
 
@@ -2017,21 +2324,21 @@ enableWhen([model.$.propertyValue, model.$.initialPayment], () => model.loanType
2017
2324
  });
2018
2325
  ```
2019
2326
 
2020
- ## 24. Key Rules
2327
+ ## 31. Key Rules
2021
2328
 
2022
2329
  1. Производные значения — через `compute`/`computeFrom` (peek-guard встроен), не ручным `onChange` + запись.
2023
2330
  2. `transformValue`-трансформер должен быть идемпотентным (`f(f(x)) === f(x)`).
2024
2331
  3. Условие `resetWhen`/`copyFrom`/`enableWhen` НЕ должно читать собственную цель.
2025
2332
  4. Расходящийся взаимный `compute` — разорви однонаправленной зависимостью или `when`.
2026
2333
 
2027
- ## 25. Purpose
2334
+ ## 32. Purpose
2028
2335
 
2029
2336
  `copyFrom` декларативно копирует значение одного поля (или группы) в другое при выполнении
2030
2337
  условия `when`. Используется для UX-сценариев «совпадает с …»: «адрес проживания = адрес
2031
2338
  регистрации», «email для уведомлений = основной email», «billing = shipping». Оператор сам
2032
2339
  выходит из reactive-контекста (`runOutsideEffect`), поэтому не порождает «Cycle detected».
2033
2340
 
2034
- ## 26. API
2341
+ ## 33. API
2035
2342
 
2036
2343
  Есть две формы. **Примитив из `@reformer/core`** (сигнал → сигнал):
2037
2344
 
@@ -2056,7 +2363,7 @@ function copyFrom<T>(
2056
2363
  `when` — реактивное условие (читает сигналы модели через `model.*`). `transform` применяется
2057
2364
  к значению перед записью в target. Отдельной опции `fields`/`debounce` нет.
2058
2365
 
2059
- ## 27. Examples
2366
+ ## 34. Examples
2060
2367
 
2061
2368
  ### Базовый сценарий — синхронизация двух адресов
2062
2369
 
@@ -2076,7 +2383,7 @@ export const orderBehavior = defineFormBehavior<OrderForm>(({ model }) => {
2076
2383
  });
2077
2384
  ```
2078
2385
 
2079
- Source: `BehaviorsExamples.tsx` (monorepo example): `copyFrom(model.$.shippingAddress, model.$.billingAddress, { when: () => model.useShippingAsBilling })`.
2386
+ Пример: `copyFrom(model.$.shippingAddress, model.$.billingAddress, { when: () => model.useShippingAsBilling })`.
2080
2387
 
2081
2388
  ### Копирование группы целиком
2082
2389
 
@@ -2124,7 +2431,7 @@ const cleanups = [
2124
2431
  // teardown: cleanups.forEach((c) => c());
2125
2432
  ```
2126
2433
 
2127
- ## 28. Anti-patterns
2434
+ ## 35. Anti-patterns
2128
2435
 
2129
2436
  ```typescript
2130
2437
  // ❌ Двусторонняя связь через два copyFrom — конфликт направлений
@@ -2143,7 +2450,7 @@ copyFrom(model.$.source, model.$.target, { when: () => model.target === '' });
2143
2450
  copyFrom(model.$.source, model.$.target, { when: () => model.copyEnabled === true });
2144
2451
  ```
2145
2452
 
2146
- ## 29. Troubleshooting
2453
+ ## 36. Troubleshooting
2147
2454
 
2148
2455
  **Q: Скопированное значение не появляется в target.**
2149
2456
  A: Проверьте, что (1) behavior передан в `createForm({ behavior })` (или примитив вызван и не
@@ -2157,14 +2464,14 @@ source и независимых флагов.
2157
2464
  A: `copyFrom` при `when === false` просто не пишет (не сбрасывает). Для сброса используй параллельно
2158
2465
  `resetWhen(model.$.target, () => !model.copyEnabled)` (см. `25-reset-when.md`).
2159
2466
 
2160
- ## 30. See also
2467
+ ## 37. See also
2161
2468
 
2162
2469
  - [24-sync-fields.md](./24-sync-fields.md) — двусторонняя синхронизация
2163
2470
  - [25-reset-when.md](./25-reset-when.md) — сброс target при выключенном условии
2164
2471
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
2165
2472
  - [22-cycle-detection.md](./22-cycle-detection.md) — почему `runOutsideEffect` защищает от циклов
2166
2473
 
2167
- ## 31. Purpose
2474
+ ## 38. Purpose
2168
2475
 
2169
2476
  `syncFields` создаёт двунаправленную связь между двумя полями: изменение любого из них
2170
2477
  переписывает второе. Применяется для дублей одного значения в разных частях формы
@@ -2172,7 +2479,7 @@ A: `copyFrom` при `when === false` просто не пишет (не сбр
2172
2479
  исключают петли. Для одностороннего копирования — [`copyFrom`](./23-copy-from.md); для
2173
2480
  расчётов — [`compute`](./20-compute-vs-watch.md).
2174
2481
 
2175
- ## 32. API
2482
+ ## 39. API
2176
2483
 
2177
2484
  Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
2178
2485
 
@@ -2187,7 +2494,7 @@ function syncFields<T>(a: Signal<T>, b: Signal<T>, options?: { transform?: (valu
2187
2494
  `a`/`b` — сигналы (`model.$.field`) совместимого типа `T`. `transform` **асимметричен**:
2188
2495
  применяется только при движении значения `a → b`. Опции `debounce`/`when` нет.
2189
2496
 
2190
- ## 33. Examples
2497
+ ## 40. Examples
2191
2498
 
2192
2499
  ### Базовый сценарий — отзеркаливание текста
2193
2500
 
@@ -2201,7 +2508,7 @@ export const mirrorBehavior = defineFormBehavior<MirrorForm>(({ model }) => {
2201
2508
  });
2202
2509
  ```
2203
2510
 
2204
- Source: `BehaviorsExamples.tsx` (monorepo example): `syncFields(model.$.syncField1, model.$.syncField2)`.
2511
+ Пример: `syncFields(model.$.syncField1, model.$.syncField2)`.
2205
2512
 
2206
2513
  ### С трансформацией — нормализация при прямой записи
2207
2514
 
@@ -2226,7 +2533,7 @@ const stop = syncFields(model.$.syncField1, model.$.syncField2);
2226
2533
  // stop() — отписаться
2227
2534
  ```
2228
2535
 
2229
- ## 34. Anti-patterns
2536
+ ## 41. Anti-patterns
2230
2537
 
2231
2538
  ```typescript
2232
2539
  // ❌ Симметрично через два copyFrom — конфликт направлений
@@ -2256,7 +2563,7 @@ syncFields(model.$.amountString, model.$.amountNumber); // string ↔ number
2256
2563
  compute(model.$.amountNumber, () => Number(model.amountString));
2257
2564
  ```
2258
2565
 
2259
- ## 35. Troubleshooting
2566
+ ## 42. Troubleshooting
2260
2567
 
2261
2568
  **Q: Поля «дёргаются», несколько перезаписей.**
2262
2569
  A: Чаще всего на одном из полей висит `transformValue`/`compute`. Убедитесь, что transform
@@ -2270,21 +2577,21 @@ A: Не вешайте дополнительно `onChange`/`watchField`, ко
2270
2577
  A: У `syncFields` нет `when`. Эмулируй через два `copyFrom(a→b, { when })` / `copyFrom(b→a, { when })`
2271
2578
  с флагами, разрешающими только одну активную сторону, либо через `apply` под условием.
2272
2579
 
2273
- ## 36. See also
2580
+ ## 43. See also
2274
2581
 
2275
2582
  - [23-copy-from.md](./23-copy-from.md) — однонаправленное копирование с `when`
2276
2583
  - [26-transform-value.md](./26-transform-value.md) — нормализация значений на месте
2277
2584
  - [22-cycle-detection.md](./22-cycle-detection.md) — почему симметричный copy ломается
2278
2585
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — `compute` для производных значений
2279
2586
 
2280
- ## 37. Purpose
2587
+ ## 44. Purpose
2281
2588
 
2282
2589
  `resetWhen` сбрасывает значение поля к `resetValue`, когда `condition` истинно. Это
2283
2590
  альтернатива `enableWhen({ resetOnDisable: true })`, когда поле остаётся **enabled**, но
2284
2591
  содержимое нужно очистить (например, переключение способа оплаты обнуляет «номер карты», но
2285
2592
  поле по-прежнему доступно). По умолчанию пишет `null`; `resetValue` задаёт произвольное значение.
2286
2593
 
2287
- ## 38. API
2594
+ ## 45. API
2288
2595
 
2289
2596
  Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
2290
2597
 
@@ -2300,7 +2607,7 @@ function resetWhen<T>(target: Signal<T>, condition: () => boolean, options?: { r
2300
2607
  `resetValue` по умолчанию `null`. Опций `onlyIfDirty`/`debounce` нет; флаги dirty/touched
2301
2608
  поля не трогаются оператором (значения принадлежат модели).
2302
2609
 
2303
- ## 39. Examples
2610
+ ## 46. Examples
2304
2611
 
2305
2612
  ### Базовый сценарий — сброс номера карты при смене способа оплаты
2306
2613
 
@@ -2314,7 +2621,7 @@ export const checkoutBehavior = defineFormBehavior<CheckoutForm>(({ model }) =>
2314
2621
  });
2315
2622
  ```
2316
2623
 
2317
- Source: `BehaviorsExamples.tsx` (monorepo example): `resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' })`.
2624
+ Пример: `resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' })`.
2318
2625
 
2319
2626
  ### resetValue для числовых полей
2320
2627
 
@@ -2336,7 +2643,7 @@ import { resetWhen } from '@reformer/core';
2336
2643
  const stop = resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
2337
2644
  ```
2338
2645
 
2339
- ## 40. Anti-patterns
2646
+ ## 47. Anti-patterns
2340
2647
 
2341
2648
  ```typescript
2342
2649
  // ❌ resetWhen вместо enableWhen для disable-сценария
@@ -2371,7 +2678,7 @@ resetWhen(model.$.cardNumber, () => model.cardNumber !== '');
2371
2678
  resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue: '' });
2372
2679
  ```
2373
2680
 
2374
- ## 41. Troubleshooting
2681
+ ## 48. Troubleshooting
2375
2682
 
2376
2683
  **Q: Сброс не срабатывает, хотя `condition` возвращает true.**
2377
2684
  A: Проверьте, что behavior зарегистрирован (`createForm({ behavior })`) или примитив не отписан,
@@ -2385,13 +2692,13 @@ A: Убедитесь, что `condition` не зависит от значен
2385
2692
  A: `resetWhen` рассчитан на скалярные сигналы. Для группы используй `enableWhen({ resetOnDisable: true })`
2386
2693
  (проходит по поддереву) или `model.reset()` для сброса всей формы к initial-снимку.
2387
2694
 
2388
- ## 42. See also
2695
+ ## 49. See also
2389
2696
 
2390
2697
  - [04-common-patterns.md](./04-common-patterns.md) — `enableWhen({ resetOnDisable: true })` как альтернатива
2391
2698
  - [23-copy-from.md](./23-copy-from.md) — копирование, у которого нет встроенного отката
2392
2699
  - [22-cycle-detection.md](./22-cycle-detection.md) — почему `condition` не должен читать целевое поле
2393
2700
 
2394
- ## 43. Purpose
2701
+ ## 50. Purpose
2395
2702
 
2396
2703
  `transformValue` подписывается на изменение поля и переписывает его трансформированной
2397
2704
  версией: uppercase для кодов, trim+toLowerCase для email, округление для чисел. Применяется
@@ -2400,7 +2707,7 @@ A: `resetWhen` рассчитан на скалярные сигналы. Для
2400
2707
  Оператор откладывает запись вне effect-контекста (`runOutsideEffect`) и не пишет, если
2401
2708
  `transformer(value) === value` — базовый guard от циклов.
2402
2709
 
2403
- ## 44. API
2710
+ ## 51. API
2404
2711
 
2405
2712
  Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
2406
2713
 
@@ -2416,7 +2723,7 @@ function transformValue<T>(target: Signal<T>, transformer: (value: T) => T): voi
2416
2723
  Дополнительных опций (`debounce`, `onUserChangeOnly`, `emitEvent`) и готового набора
2417
2724
  `transformers`/`createTransformer` НЕТ — трансформер пишется как обычная функция.
2418
2725
 
2419
- ## 45. Examples
2726
+ ## 52. Examples
2420
2727
 
2421
2728
  ### Базовый сценарий — uppercase для кода
2422
2729
 
@@ -2430,7 +2737,7 @@ export const promoBehavior = defineFormBehavior<PromoForm>(({ model }) => {
2430
2737
  });
2431
2738
  ```
2432
2739
 
2433
- Source: `BehaviorsExamples.tsx` (monorepo example): `transformValue(model.$.uppercaseField, (v) => (v ?? '').toUpperCase())`.
2740
+ Пример: `transformValue(model.$.uppercaseField, (v) => (v ?? '').toUpperCase())`.
2434
2741
 
2435
2742
  ### Несколько трансформаций
2436
2743
 
@@ -2478,7 +2785,7 @@ toUpper(model.$.promoCode);
2478
2785
  trim(model.$.username);
2479
2786
  ```
2480
2787
 
2481
- ## 46. Anti-patterns
2788
+ ## 53. Anti-patterns
2482
2789
 
2483
2790
  ```typescript
2484
2791
  // ❌ Неидемпотентный transformer — бесконечный цикл
@@ -2506,7 +2813,7 @@ transformValue(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
2506
2813
  compute(model.$.fullName, () => `${model.firstName} ${model.lastName}`);
2507
2814
  ```
2508
2815
 
2509
- ## 47. Troubleshooting
2816
+ ## 54. Troubleshooting
2510
2817
 
2511
2818
  **Q: «Cycle detected» при transformValue.**
2512
2819
  A: 99% — неидемпотентный transformer. Проверь `transformer(transformer(x)) === transformer(x)`.
@@ -2519,14 +2826,14 @@ A: Проверь, что behavior зарегистрирован (`createForm({
2519
2826
  A: Симптом частых записей. Форматирование с динамическими разделителями (телефон, кредитка)
2520
2827
  лучше делать через `<InputMask>` из `@reformer/ui-kit`, а не `transformValue`.
2521
2828
 
2522
- ## 48. See also
2829
+ ## 55. See also
2523
2830
 
2524
2831
  - [24-sync-fields.md](./24-sync-fields.md) — порядок применения с syncFields
2525
2832
  - [23-copy-from.md](./23-copy-from.md) — `transform`-опция при копировании
2526
2833
  - [22-cycle-detection.md](./22-cycle-detection.md) — про идемпотентность
2527
2834
  - [16-ui-components.md](./16-ui-components.md) — `<InputMask>` для тяжёлого форматирования
2528
2835
 
2529
- ## 49. Purpose
2836
+ ## 56. Purpose
2530
2837
 
2531
2838
  `revalidateWhen` вызывает переданный колбэк ревалидации, когда изменяется любая из
2532
2839
  зависимостей. Применяется, когда правило зависит от значений других полей
@@ -2534,7 +2841,7 @@ A: Симптом частых записей. Форматирование с
2534
2841
  Под M1 валидация on-demand (`validateFormModel`), поэтому ревалидация выражается явным
2535
2842
  колбэком, а не автоматической привязкой к полю.
2536
2843
 
2537
- ## 50. API
2844
+ ## 57. API
2538
2845
 
2539
2846
  Одинаково в примитиве (`@reformer/core`) и DSL (`@reformer/core/behaviors`):
2540
2847
 
@@ -2550,7 +2857,7 @@ function revalidateWhen(deps: ReadonlySignal<unknown>[], revalidate: () => void)
2550
2857
  `() => validateFormModel(model, schema)`). Вызывается при изменении любой зависимости
2551
2858
  (НЕ на инициализации), вне effect-контекста.
2552
2859
 
2553
- ## 51. Examples
2860
+ ## 58. Examples
2554
2861
 
2555
2862
  ### Базовый сценарий — перевалидация amount при смене maxAmount
2556
2863
 
@@ -2565,7 +2872,7 @@ export const paymentBehavior = defineFormBehavior<PaymentForm>(({ model }) => {
2565
2872
  });
2566
2873
  ```
2567
2874
 
2568
- Source: `BehaviorsExamples.tsx` (monorepo example): `revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema))`.
2875
+ Пример: `revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema))`.
2569
2876
 
2570
2877
  ### Несколько триггеров
2571
2878
 
@@ -2603,7 +2910,7 @@ import { revalidateWhen, validateFormModel } from '@reformer/core';
2603
2910
  const stop = revalidateWhen([model.$.maxAmount], () => void validateFormModel(model, schema));
2604
2911
  ```
2605
2912
 
2606
- ## 52. Anti-patterns
2913
+ ## 59. Anti-patterns
2607
2914
 
2608
2915
  ```typescript
2609
2916
  // ❌ Триггер == поле, которое и так меняется само
@@ -2626,7 +2933,7 @@ const amountVsMax: ModelValidator<number, unknown, Form> = (v, _s, root) =>
2626
2933
  revalidateWhen([model.$.maxAmount], () => validateFormModel(model, schema));
2627
2934
  ```
2628
2935
 
2629
- ## 53. Troubleshooting
2936
+ ## 60. Troubleshooting
2630
2937
 
2631
2938
  **Q: Ошибка target не пропадает после изменения триггера.**
2632
2939
  A: Проверьте, что (1) правило реально читает значение триггера через `root` (cross-field
@@ -2636,20 +2943,20 @@ A: Проверьте, что (1) правило реально читает з
2636
2943
  **Q: Как перевалидировать только одно поле, а не всю схему?**
2637
2944
  A: Передай в колбэк под-схему только этого поля: `validateFormModel(model, { children: [fieldNode] })`.
2638
2945
 
2639
- ## 54. See also
2946
+ ## 61. See also
2640
2947
 
2641
2948
  - [03-api-signatures.md](./03-api-signatures.md) — cross-field `ModelValidator` и `validateFormModel`
2642
2949
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — полная валидация формы
2643
2950
  - [20-compute-vs-watch.md](./20-compute-vs-watch.md) — реактивные производные значения
2644
2951
 
2645
- ## 55. Purpose
2952
+ ## 62. Purpose
2646
2953
 
2647
2954
  Канонический submit-флоу под M1: «запустить полную валидацию данных (`validateFormModel`) →
2648
2955
  проверить `result.valid` → достать снимок (`model.get()`) → сделать запрос → `model.reset()`».
2649
2956
  Валидация — чистая функция ДАННЫХ (модели), она же роутит ошибки в ноды формы, поэтому UI
2650
2957
  подсветит проблемные поля. `model.reset()` возвращает значения к initial-снимку.
2651
2958
 
2652
- ## 56. API
2959
+ ## 63. API
2653
2960
 
2654
2961
  ```typescript
2655
2962
  // Модель (источник истины значений):
@@ -2671,7 +2978,7 @@ form.<field>.markAsTouched();
2671
2978
  form.<field>.reset();
2672
2979
  ```
2673
2980
 
2674
- ## 57. Examples
2981
+ ## 64. Examples
2675
2982
 
2676
2983
  ### Базовый submit-handler
2677
2984
 
@@ -2784,7 +3091,7 @@ async function processFormPayload(rawData: Partial<MyForm>) {
2784
3091
  }
2785
3092
  ```
2786
3093
 
2787
- ## 58. Anti-patterns
3094
+ ## 65. Anti-patterns
2788
3095
 
2789
3096
  ```typescript
2790
3097
  // ❌ Чтение результата валидации без await
@@ -2824,7 +3131,7 @@ model.password = '';
2824
3131
  model.reset();
2825
3132
  ```
2826
3133
 
2827
- ## 59. Troubleshooting
3134
+ ## 66. Troubleshooting
2828
3135
 
2829
3136
  **Q: После `reset()` в UI остались старые ошибки.**
2830
3137
  A: `model.reset()` меняет значения; ошибки в нодах чистит валидация. Перезапусти
@@ -2839,14 +3146,14 @@ A: `reset()` возвращает к initial-снимку (значения на
2839
3146
  A: `model.get()` возвращает значения всех полей. Фильтруй вручную после `get()` или используй
2840
3147
  `enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial.
2841
3148
 
2842
- ## 60. See also
3149
+ ## 67. See also
2843
3150
 
2844
3151
  - [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
2845
3152
  - [13-multi-step.md](./13-multi-step.md) — пошаговая валидация через `validateFormModel`
2846
3153
  - [03-api-signatures.md](./03-api-signatures.md) — сигнатуры модели и `validateFormModel`
2847
3154
  - [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки
2848
3155
 
2849
- ## 61. Purpose
3156
+ ## 68. Purpose
2850
3157
 
2851
3158
  Загрузка формы данными с сервера под M1: (1) **initial values** в `createModel(...)`; (2)
2852
3159
  **`model.set` / `model.patch`** для загрузки/обновления значений; (3) **external React-hook**
@@ -2855,7 +3162,7 @@ race-guard через deps). Динамические `componentProps` (опци
2855
3162
  `form.field.updateComponentProps({ options })` в `queueMicrotask`, чтобы не пересечься с
2856
3163
  реактивными эффектами от `set`/`patch`.
2857
3164
 
2858
- ## 62. API
3165
+ ## 69. API
2859
3166
 
2860
3167
  ```typescript
2861
3168
  // Модель:
@@ -2871,7 +3178,7 @@ form.field.updateComponentProps(props: Record<string, unknown>): void; // дин
2871
3178
  Initial values задаются в `createModel(initial)`; `reset()` возвращает к ним. Чтобы загруженные
2872
3179
  данные стали новой «точкой отсчёта» для `reset()` — вызови `model.captureInitial()` после load.
2873
3180
 
2874
- ## 63. Examples
3181
+ ## 70. Examples
2875
3182
 
2876
3183
  ### Initial values в модели
2877
3184
 
@@ -2978,7 +3285,7 @@ export const addressBehavior = defineFormBehavior<AddressForm>(({ model, form })
2978
3285
  });
2979
3286
  ```
2980
3287
 
2981
- ## 64. Anti-patterns
3288
+ ## 71. Anti-patterns
2982
3289
 
2983
3290
  ```typescript
2984
3291
  // ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз
@@ -3018,7 +3325,7 @@ useEffect(() => {
3018
3325
  }, [id]);
3019
3326
  ```
3020
3327
 
3021
- ## 65. Troubleshooting
3328
+ ## 72. Troubleshooting
3022
3329
 
3023
3330
  **Q: После `set`/`patch` не показываются ошибки.**
3024
3331
  A: `set`/`patch` не запускают валидацию. После загрузки вызови `await validateFormModel(model, schema)`.
@@ -3031,14 +3338,14 @@ A: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что
3031
3338
  A: `reset()` возвращает к initial-снимку из `createModel`, не к последнему `set`. Чтобы «сбросить
3032
3339
  к данным с сервера» — после `set` вызови `model.captureInitial()` (новая точка отсчёта).
3033
3340
 
3034
- ## 66. See also
3341
+ ## 73. See also
3035
3342
 
3036
3343
  - [28-submit-and-reset.md](./28-submit-and-reset.md) — обратная сторона жизненного цикла
3037
3344
  - [11-async-watchfield.md](./11-async-watchfield.md) — `onChange` для динамики после preload
3038
3345
  - [22-cycle-detection.md](./22-cycle-detection.md) — почему `queueMicrotask` нужен
3039
3346
  - [16-ui-components.md](./16-ui-components.md) — `updateComponentProps` для динамических опций
3040
3347
 
3041
- ## 67. 30. TYPE-SAFETY RECIPES
3348
+ ## 74. 30. TYPE-SAFETY RECIPES
3042
3349
 
3043
3350
  Идиоматичные паттерны, которые держат сгенерированный код без `any` и `as`-кастов под M1.
3044
3351
 
@@ -3189,7 +3496,7 @@ const schema = {
3189
3496
  - `as`-касты значений полей внутри `compute` → читай `model.field` напрямую (Recipe 5).
3190
3497
  - строковые пути / `(form) => ...` в behaviors → это удалённый API, используй сигналы (`model.$.x`).
3191
3498
 
3192
- ## 68. 31. ASYNC VALIDATOR
3499
+ ## 75. 31. ASYNC VALIDATOR
3193
3500
 
3194
3501
  Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
3195
3502
  async-валидатор это `ModelValidator`, возвращающий `Promise<ValidationError | null>`. Он
@@ -3263,7 +3570,7 @@ return pending ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
3263
3570
  - [29-async-preload.md](29-async-preload.md) — async preload данных при init формы
3264
3571
  - [03-api-signatures.md](03-api-signatures.md) — `ModelValidator` и `validateFormModel`
3265
3572
 
3266
- ## 69. 32. ASYNC OPTIONS LOADING
3573
+ ## 76. 32. ASYNC OPTIONS LOADING
3267
3574
 
3268
3575
  Для динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,
3269
3576
  `carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +
@@ -3372,7 +3679,7 @@ loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: tr
3372
3679
  - [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
3373
3680
  - [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)
3374
3681
 
3375
- ## 70. API Reference
3682
+ ## 77. API Reference
3376
3683
 
3377
3684
  _Auto-generated from JSDoc on public exports._
3378
3685
 
@@ -3398,7 +3705,7 @@ _Source: src/core/types/index.ts_
3398
3705
 
3399
3706
  **Signature:**
3400
3707
  ```typescript
3401
- export interface ArrayConfig<T extends FormFields> {
3708
+ export interface ArrayConfig<T extends object> {
3402
3709
  itemSchema: FormSchema<T>;
3403
3710
  initial?: Partial<T>[];
3404
3711
  }
@@ -3605,16 +3912,16 @@ _Source: src/core/types/index.ts_
3605
3912
 
3606
3913
  **Kind:** `type`
3607
3914
 
3608
- Чистый асинхронный валидатор поля.
3609
-
3610
- Регистрируется через `validateAsync(path, validator, { debounce })`.
3915
+ Чистый асинхронный валидатор поля. Та же тройка аргументов `(value, scope, root)`, что у
3916
+ {@link Validator}, но возвращает `Promise`. Регистрируется через
3917
+ `validateAsync(path, validator, { debounce })`.
3611
3918
 
3612
3919
  **Signature:**
3613
3920
  ```typescript
3614
3921
  export type AsyncValidator<TForm, TField> = (
3615
3922
  value: TField,
3616
- control: FormProxy<TField>,
3617
- root: FormProxy<TForm>
3923
+ scope: unknown,
3924
+ root: FormModel<TForm>
3618
3925
  ) => Promise<ValidationError | null>;
3619
3926
  ```
3620
3927
 
@@ -3916,10 +4223,10 @@ export interface CreateFormFromModelArgs<T> {
3916
4223
  /** Реактивная модель данных (источник истины значений). */
3917
4224
  model: FormModel<T>;
3918
4225
  /**
3919
- * Единая Schema (дерево узлов). createForm обходит её и привязывает конфиг поля к ноде
3920
- * по идентичности сигнала (`node.value === model.$.path`). Опциональна.
4226
+ * Единая Schema (дерево узлов {@link FormSchemaNode}). createForm обходит её и привязывает конфиг
4227
+ * поля к ноде по идентичности сигнала (`node.value === model.$.path`). Опциональна.
3921
4228
  */
3922
- schema?: unknown;
4229
+ schema?: FormSchemaNode;
3923
4230
  /**
3924
4231
  * Декларативная схема поведения ({@link defineFormBehavior}). Запускается ПОСЛЕ построения нод и
3925
4232
  * заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
@@ -4072,7 +4379,7 @@ export interface ErrorFilterOptions {
4072
4379
  message?: string;
4073
4380
 
4074
4381
  /** Фильтр по параметрам ошибки */
4075
- params?: FormFields;
4382
+ params?: Record<string, FormValue>;
4076
4383
 
4077
4384
  /** Кастомный предикат для фильтрации */
4078
4385
  predicate?: (error: ValidationError) => boolean;
@@ -4325,8 +4632,7 @@ export interface FieldControlState<T> {
4325
4632
  * );
4326
4633
  * ```
4327
4634
  */
4328
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4329
- componentProps: Record<string, any>;
4635
+ componentProps: Record<string, unknown>;
4330
4636
  }
4331
4637
  ```
4332
4638
 
@@ -4590,22 +4896,6 @@ try {
4590
4896
 
4591
4897
  _Source: src/core/utils/error-handler.ts_
4592
4898
 
4593
- ### FormFields
4594
-
4595
- **Kind:** `type`
4596
-
4597
- Тип для конфига с полями (FormSchema generic constraint)
4598
- Используется вместо `Record<string, any>` для схем форм
4599
-
4600
- **Signature:**
4601
- ```typescript
4602
- export type FormFields = Record<string, FormValue>;
4603
- ```
4604
-
4605
- **Deprecated:** (no message)
4606
-
4607
- _Source: src/core/types/index.ts_
4608
-
4609
4899
  ### FormModel
4610
4900
 
4611
4901
  **Kind:** `type`
@@ -4702,12 +4992,17 @@ _Source: src/core/types/form-proxy.ts_
4702
4992
 
4703
4993
  **Kind:** `type`
4704
4994
 
4705
- Автоматически определяет тип схемы на основе TypeScript типа:
4995
+ **Data-shaped** конфиг формы: ключи повторяют структуру данных `T`, значения — {@link FieldConfig}
4996
+ (или вложенный `FormSchema`). Форма конфига, из которой строится {@link GroupNode}.
4706
4997
  - `T[] -> [FormSchema<T>]` (массив с одним элементом)
4707
4998
  - `object -> FormSchema<T>` (группа)
4708
4999
  - `primitive -> FieldConfig<T>` (поле)
4709
5000
 
4710
- Использует NonNullable для корректной обработки опциональных полей
5001
+ Использует NonNullable для корректной обработки опциональных полей.
5002
+
5003
+ ⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
5004
+ контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
5005
+ (ключи = поля данных), а не узел дерева.
4711
5006
 
4712
5007
  **Signature:**
4713
5008
  ```typescript
@@ -4756,6 +5051,65 @@ const schema: FormSchema<Form> = {
4756
5051
 
4757
5052
  _Source: src/core/types/deep-schema.ts_
4758
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
+
4759
5113
  ### FormStatusMachine
4760
5114
 
4761
5115
  **Kind:** `class`
@@ -4801,7 +5155,7 @@ FormSubmitter - управляет процессом отправки форм
4801
5155
 
4802
5156
  **Signature:**
4803
5157
  ```typescript
4804
- export class FormSubmitter<T extends FormFields> {
5158
+ export class FormSubmitter<T extends object> {
4805
5159
  /** Внутренний сигнал состояния отправки */ /* … */ }
4806
5160
  ```
4807
5161
 
@@ -5009,7 +5363,7 @@ _Source: src/core/types/index.ts_
5009
5363
 
5010
5364
  **Signature:**
5011
5365
  ```typescript
5012
- export function integer<TForm = unknown, TField extends number | undefined = number>(
5366
+ export function integer<TForm = unknown, TField extends number | null | undefined = number>(
5013
5367
  options?: ValidateOptions
5014
5368
  ): Validator<TForm, TField>
5015
5369
  ```
@@ -5046,7 +5400,7 @@ ArrayNode представляет массив вложенных форм (о
5046
5400
 
5047
5401
  **Signature:**
5048
5402
  ```typescript
5049
- export function isArrayNode(value: unknown): value is ArrayNode<FormFields>
5403
+ export function isArrayNode(value: unknown): value is ArrayNode<object>
5050
5404
  ```
5051
5405
 
5052
5406
  **Parameters:**
@@ -5208,7 +5562,7 @@ GroupNode представляет объект с вложенными поля
5208
5562
 
5209
5563
  **Signature:**
5210
5564
  ```typescript
5211
- export function isGroupNode(value: unknown): value is GroupNode<FormFields>
5565
+ export function isGroupNode(value: unknown): value is GroupNode<object>
5212
5566
  ```
5213
5567
 
5214
5568
  **Parameters:**
@@ -5238,7 +5592,7 @@ _Source: src/core/utils/type-guards.ts_
5238
5592
 
5239
5593
  **Signature:**
5240
5594
  ```typescript
5241
- export function isNumber<TForm = unknown, TField extends number | undefined = number>(
5595
+ export function isNumber<TForm = unknown, TField extends number | null | undefined = number>(
5242
5596
  options?: ValidateOptions
5243
5597
  ): Validator<TForm, TField>
5244
5598
  ```
@@ -5306,7 +5660,7 @@ _Source: src/core/utils/derived-registry.ts_
5306
5660
 
5307
5661
  **Signature:**
5308
5662
  ```typescript
5309
- export function max<TForm = unknown, TField extends number | undefined = number>(
5663
+ export function max<TForm = unknown, TField extends number | null | undefined = number>(
5310
5664
  maxValue: number,
5311
5665
  options?: ValidateOptions
5312
5666
  ): Validator<TForm, TField>
@@ -5468,7 +5822,7 @@ _Source: src/core/validation/validators/max-length.ts_
5468
5822
 
5469
5823
  **Signature:**
5470
5824
  ```typescript
5471
- export function min<TForm = unknown, TField extends number | undefined = number>(
5825
+ export function min<TForm = unknown, TField extends number | null | undefined = number>(
5472
5826
  minValue: number,
5473
5827
  options?: ValidateOptions
5474
5828
  ): Validator<TForm, TField>
@@ -5636,9 +5990,17 @@ export interface ModelApi<T> {
5636
5990
  readonly $: ModelSignals<T>;
5637
5991
  /** Снимок значений (без подписки) — для submit. */
5638
5992
  get(): T;
5639
- /** Массовая замена значений (load с сервера). Не меняет initial-снимок. */
5993
+ /**
5994
+ * @deprecated Алиас {@link ModelApi.patch}: `set` выполняет то же частичное **слияние** —
5995
+ * обновляет только переданные ключи, отсутствующие НЕ трогает (это НЕ полная замена, вопреки
5996
+ * старому названию). Используйте `patch`; для замены с обнулением лишних полей делайте
5997
+ * `patch({ ...defaults, ...dto })` явно. Не меняет initial-снимок.
5998
+ */
5640
5999
  set(value: Partial<T>): void;
5641
- /** Частичное слияние значений. */
6000
+ /**
6001
+ * Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
6002
+ * отсутствующие ключи НЕ трогаются. Не меняет initial-снимок.
6003
+ */
5642
6004
  patch(value: Partial<T>): void;
5643
6005
  /** Отличаются ли текущие значения от initial-снимка (value-diff). */
5644
6006
  isDirty(): boolean;
@@ -5663,6 +6025,11 @@ _Source: src/core/model/types.ts_
5663
6025
  **Signature:**
5664
6026
  ```typescript
5665
6027
  export interface ModelArray<U> {
6028
+ /**
6029
+ * Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
6030
+ * рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
6031
+ */
6032
+ readonly __path: string;
5666
6033
  /** Реактивная длина. */
5667
6034
  readonly length: number;
5668
6035
  /** Добавить элемент в конец (значение элемента целиком). */
@@ -5677,12 +6044,14 @@ export interface ModelArray<U> {
5677
6044
  swap(a: number, b: number): void;
5678
6045
  /** Очистить массив. */
5679
6046
  clear(): void;
5680
- /** Под-модель элемента по индексу (для объектных элементов) или значение (для примитивных). */
5681
- at(index: number): U extends object ? FormModel<U> : U | undefined;
5682
- /** Map по элементам: объектные → {@link FormModel}, примитивные → значение. */
5683
- map<R>(fn: (item: U extends object ? FormModel<U> : U, index: number) => R): R[];
5684
- /** Итерация по элементам. */
5685
- 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;
5686
6055
  /** Снимок массива значений (без подписки). */
5687
6056
  toArray(): U[];
5688
6057
  /** Индексный value-доступ. */
@@ -5789,13 +6158,7 @@ _Source: src/core/model/types.ts_
5789
6158
  **Signature:**
5790
6159
  ```typescript
5791
6160
  export type ModelSignals<T> = {
5792
- [K in keyof T]: NonNullable<T[K]> extends ReadonlyArray<infer U>
5793
- ? { readonly length: number; readonly [index: number]: ModelSignals<U & object> }
5794
- : NonNullable<T[K]> extends Opaque
5795
- ? PathAwareSignal<T[K]>
5796
- : NonNullable<T[K]> extends object
5797
- ? ModelSignals<NonNullable<T[K]>>
5798
- : PathAwareSignal<T[K]>;
6161
+ [K in keyof T]: ModelSignalNode<T[K]>;
5799
6162
  };
5800
6163
  ```
5801
6164
 
@@ -5822,9 +6185,12 @@ _Source: src/core/model/validate-model.ts_
5822
6185
 
5823
6186
  **Kind:** `type`
5824
6187
 
5825
- Валидатор слоя данных. `value` — значение поля; `model` — ближайший scope (под-модель элемента
6188
+ Валидатор слоя **данных**. `value` — значение поля; `model` — ближайший scope (под-модель элемента
5826
6189
  массива или корень); `root` — корневая модель.
5827
6190
 
6191
+ Отличие от {@link Validator}: 2-й/3-й аргументы — сами данные ({@link FormModel}/scope), а не
6192
+ `FormProxy`-узлы формы. Оба совместимы с полем `validators` узла схемы (см. {@link SchemaValidator}).
6193
+
5828
6194
  **Signature:**
5829
6195
  ```typescript
5830
6196
  export type ModelValidator<TValue = unknown, TModel = unknown, TRoot = unknown> = (
@@ -5870,7 +6236,7 @@ _Source: src/core/model/types.ts_
5870
6236
 
5871
6237
  **Signature:**
5872
6238
  ```typescript
5873
- export function multipleOf<TForm = unknown, TField extends number | undefined = number>(
6239
+ export function multipleOf<TForm = unknown, TField extends number | null | undefined = number>(
5874
6240
  divisor: number,
5875
6241
  options?: ValidateOptions
5876
6242
  ): Validator<TForm, TField>
@@ -5979,7 +6345,7 @@ _Source: src/core/factories/node-factory.ts_
5979
6345
 
5980
6346
  **Signature:**
5981
6347
  ```typescript
5982
- export function nonNegative<TForm = unknown, TField extends number | undefined = number>(
6348
+ export function nonNegative<TForm = unknown, TField extends number | null | undefined = number>(
5983
6349
  options?: ValidateOptions
5984
6350
  ): Validator<TForm, TField>
5985
6351
  ```
@@ -6015,7 +6381,7 @@ _Source: src/core/validation/validators/non-negative.ts_
6015
6381
 
6016
6382
  **Signature:**
6017
6383
  ```typescript
6018
- export function nonZero<TForm = unknown, TField extends number | undefined = number>(
6384
+ export function nonZero<TForm = unknown, TField extends number | null | undefined = number>(
6019
6385
  options?: ValidateOptions
6020
6386
  ): Validator<TForm, TField>
6021
6387
  ```
@@ -6441,6 +6807,48 @@ return effect(() => {
6441
6807
 
6442
6808
  _Source: src/core/utils/safe-effect.ts_
6443
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
+
6444
6852
  ### SetValueOptions
6445
6853
 
6446
6854
  **Kind:** `interface`
@@ -6525,7 +6933,7 @@ _Source: src/core/utils/form-submitter.ts_
6525
6933
 
6526
6934
  **Signature:**
6527
6935
  ```typescript
6528
- export interface SubmittableForm<T extends FormFields> {
6936
+ export interface SubmittableForm<T extends object> {
6529
6937
  /** Пометить все поля как touched */
6530
6938
  markAsTouched(): void;
6531
6939
  /** Валидировать форму */
@@ -6691,7 +7099,7 @@ _Source: src/core/types/index.ts_
6691
7099
 
6692
7100
  **Signature:**
6693
7101
  ```typescript
6694
- export type UnknownRecord = Record<string, any>;
7102
+ export type UnknownRecord = Record<string, unknown>;
6695
7103
  ```
6696
7104
 
6697
7105
  _Source: src/core/types/index.ts_
@@ -6752,7 +7160,7 @@ React-хук для подписки только на длину массива
6752
7160
 
6753
7161
  **Signature:**
6754
7162
  ```typescript
6755
- export function useArrayLength<T extends FormFields>(control: ArrayNode<T>): number
7163
+ export function useArrayLength<T extends object>(control: ArrayNode<T>): number
6756
7164
  ```
6757
7165
 
6758
7166
  **Parameters:**
@@ -6806,8 +7214,8 @@ React-хук для подписки на состояние формы (FieldNo
6806
7214
  **Signature:**
6807
7215
  ```typescript
6808
7216
  export function useFormControl(
6809
- control: FieldNode<FormValue> | ArrayNode<FormFields> | undefined
6810
- ): FieldControlState<FormValue> | ArrayControlState<FormFields>
7217
+ control: FieldNode<FormValue> | ArrayNode<object> | undefined
7218
+ ): FieldControlState<FormValue> | ArrayControlState<object>
6811
7219
  ```
6812
7220
 
6813
7221
  **Parameters:**
@@ -7221,7 +7629,7 @@ In-form валидация: прогоняет {@link validateModel} и роут
7221
7629
  ```typescript
7222
7630
  export async function validateFormModel<T>(
7223
7631
  model: FormModel<T>,
7224
- schema: unknown
7632
+ schema: FormSchemaNode
7225
7633
  ): Promise<ModelValidationResult>
7226
7634
  ```
7227
7635
 
@@ -7253,7 +7661,7 @@ Headless-валидация данных (sync + async). Работает без
7253
7661
  ```typescript
7254
7662
  export async function validateModel<T>(
7255
7663
  model: FormModel<T>,
7256
- schema: unknown
7664
+ schema: FormSchemaNode
7257
7665
  ): Promise<ModelValidationResult>
7258
7666
  ```
7259
7667
 
@@ -7274,7 +7682,10 @@ _Source: src/core/model/validate-model.ts_
7274
7682
 
7275
7683
  **Signature:**
7276
7684
  ```typescript
7277
- export function validateModelSync<T>(model: FormModel<T>, schema: unknown): ModelValidationResult
7685
+ export function validateModelSync<T>(
7686
+ model: FormModel<T>,
7687
+ schema: FormSchemaNode
7688
+ ): ModelValidationResult
7278
7689
  ```
7279
7690
 
7280
7691
  **Parameters:**
@@ -7306,7 +7717,7 @@ export interface ValidateOptions {
7306
7717
  /** Сообщение об ошибке */
7307
7718
  message?: string;
7308
7719
  /** Параметры ошибки */
7309
- params?: FormFields;
7720
+ params?: Record<string, FormValue>;
7310
7721
  }
7311
7722
  ```
7312
7723
 
@@ -7323,7 +7734,7 @@ _Source: src/core/types/validation-schema.ts_
7323
7734
  export interface ValidationError {
7324
7735
  code: string;
7325
7736
  message: string;
7326
- params?: FormFields;
7737
+ params?: Record<string, FormValue>;
7327
7738
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
7328
7739
  severity?: 'error' | 'warning';
7329
7740
  }
@@ -7337,15 +7748,25 @@ _Source: src/core/types/index.ts_
7337
7748
 
7338
7749
  Чистый синхронный валидатор поля.
7339
7750
 
7340
- Принимает значение поля, прокси текущего поля (control) и прокси корня формы (root).
7341
- Возвращает 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
+ дополнительные аргументы игнорируют.
7342
7763
 
7343
7764
  **Signature:**
7344
7765
  ```typescript
7345
7766
  export type Validator<TForm, TField> = (
7346
7767
  value: TField,
7347
- control: FormProxy<TField>,
7348
- root: FormProxy<TForm>
7768
+ scope: unknown,
7769
+ root: FormModel<TForm>
7349
7770
  ) => ValidationError | null;
7350
7771
  ```
7351
7772
 
@@ -7353,8 +7774,9 @@ export type Validator<TForm, TField> = (
7353
7774
 
7354
7775
  Кастомный валидатор в массиве `validators` поля схемы
7355
7776
  ```typescript
7356
- const isAdult: Validator<MyForm, number> = (value, control, root) => {
7777
+ const isAdult: Validator<MyForm, number> = (value, _scope, root) => {
7357
7778
  if (value < 18) return { code: 'tooYoung', message: '18+' };
7779
+ // root — FormModel<MyForm>: root.someOtherField читается как значение
7358
7780
  return null;
7359
7781
  };
7360
7782