@ryuzaki13/react-foundation-lib 1.3.1 → 1.4.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 (38) hide show
  1. package/README.md +20 -16
  2. package/dist/table/index.d.ts +1 -0
  3. package/dist/table/index.d.ts.map +1 -1
  4. package/dist/table/index.js +77 -64
  5. package/dist/table/index.js.map +1 -1
  6. package/dist/table/tanStackFeatures.d.ts +19 -0
  7. package/dist/table/tanStackFeatures.d.ts.map +1 -0
  8. package/dist/table/types.d.ts +17 -2
  9. package/dist/table/types.d.ts.map +1 -1
  10. package/package.json +9 -9
  11. package/src/array/README.mdx +1630 -0
  12. package/src/binary/README.mdx +1405 -0
  13. package/src/copy/README.mdx +992 -0
  14. package/src/crypto/README.mdx +1052 -0
  15. package/src/dom/README.mdx +2179 -0
  16. package/src/file/README.mdx +1957 -0
  17. package/src/formatters/README.mdx +469 -0
  18. package/src/formatters/boolean/README.mdx +494 -0
  19. package/src/formatters/date/README.mdx +732 -0
  20. package/src/formatters/number/README.mdx +542 -0
  21. package/src/formatters/pipeline/README.mdx +629 -0
  22. package/src/formatters/rowBased/README.mdx +486 -0
  23. package/src/formatters/strings/README.mdx +442 -0
  24. package/src/formatters/valueState/README.mdx +459 -0
  25. package/src/formulas/README.mdx +1283 -0
  26. package/src/binary/README.md +0 -110
  27. package/src/docs/browserAndReact.mdx +0 -150
  28. package/src/docs/filesAndExcel.mdx +0 -163
  29. package/src/docs/formatters-runtime.mdx +0 -455
  30. package/src/docs/formatters.mdx +0 -380
  31. package/src/docs/odataAndTables.mdx +0 -178
  32. package/src/docs/pureUtilities.mdx +0 -96
  33. package/src/docs/runtimeAndState.mdx +0 -155
  34. package/src/file/README.md +0 -289
  35. package/src/formatters/date/README.md +0 -520
  36. package/src/formatters/number/README.md +0 -123
  37. package/src/formatters/valueState/README.md +0 -275
  38. package/src/formulas/README.md +0 -42
@@ -0,0 +1,1283 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/API/Formulas" />
4
+
5
+ # Табличные формулы
6
+
7
+ Модуль `@ryuzaki13/react-foundation-lib/formulas` предоставляет безопасную инфраструктуру для заранее написанных клиентских формул таблицы.
8
+
9
+ Формула получает значения других полей текущей строки, выполняет синхронный расчёт и возвращает конечное JavaScript-число. Например, она может вычислить сумму, разницу, отношение или процент для calculated-колонки.
10
+
11
+ ```text
12
+ данные строки + упорядоченные зависимости
13
+ ↓
14
+ зарегистрированная формула
15
+ ↓
16
+ конечное число или
17
+ контролируемая причина ошибки выполнения
18
+ ```
19
+
20
+ README рассчитан на разработчика без доступа к исходному коду. Здесь описано точное поведение всего публичного API, включая преобразование значений, реестр, validation, ошибки и связь с formatter pipeline.
21
+
22
+ ## Содержание
23
+
24
+ - [Назначение и границы](#назначение-и-границы)
25
+ - [Установка и импорт](#установка-и-импорт)
26
+ - [Быстрый старт](#быстрый-старт)
27
+ - [Основные понятия](#основные-понятия)
28
+ - [Описание формулы](#описание-формулы)
29
+ - [Контекст формулы](#контекст-формулы)
30
+ - [Числовое преобразование `ctx.num`](#числовое-преобразование-ctxnum)
31
+ - [Создание и установка реестра](#создание-и-установка-реестра)
32
+ - [Чтение активного реестра](#чтение-активного-реестра)
33
+ - [Выполнение формулы](#выполнение-формулы)
34
+ - [Предварительная компиляция](#предварительная-компиляция)
35
+ - [Валидация зависимостей](#валидация-зависимостей)
36
+ - [Интеграция с formatter pipeline](#интеграция-с-formatter-pipeline)
37
+ - [Отличие от row-based formatter](#отличие-от-row-based-formatter)
38
+ - [Архитектура и безопасность](#архитектура-и-безопасность)
39
+ - [Тестирование](#тестирование)
40
+ - [Производительность](#производительность)
41
+ - [Частые ошибки](#частые-ошибки)
42
+ - [API-справка](#api-справка)
43
+
44
+ ## Назначение и границы
45
+
46
+ ### Что делает модуль
47
+
48
+ - описывает типизированный контракт числовой формулы;
49
+ - связывает dependency indexes с полями текущей строки;
50
+ - создаёт нормализованный реестр формул host-приложения;
51
+ - хранит один активный реестр в текущем экземпляре модуля;
52
+ - выполняет формулы без выбрасывания их runtime-ошибок наружу;
53
+ - компилирует formula + keys в переиспользуемый executor;
54
+ - проверяет существование формулы и корректность выбранных dependencies;
55
+ - сообщает, какие dependency indexes реально использовала формула на тестовых данных.
56
+
57
+ ### Чего модуль не делает
58
+
59
+ - не содержит встроенного каталога бизнес-формул;
60
+ - не разбирает текст математического выражения;
61
+ - не использует `eval` или `new Function`;
62
+ - не выполняет произвольный пользовательский JavaScript;
63
+ - не загружает зависимости с сервера;
64
+ - не добавляет нужные поля в OData/HTTP-запрос;
65
+ - не форматирует результат для UI;
66
+ - не выбирает fallback для конкретной таблицы;
67
+ - не поддерживает асинхронные формулы;
68
+ - не хранит function body в JSON или БД.
69
+
70
+ Host-приложение владеет прикладными `TableFormulaDefinition`: оно объявляет их в коде, тестирует, собирает в реестр и устанавливает на старте приложения.
71
+
72
+ Сериализуемая конфигурация колонки хранит только стабильный `formulaId` и ordered dependencies. Реальная функция остаётся доверенным кодом приложения.
73
+
74
+ ## Установка и импорт
75
+
76
+ ```bash
77
+ npm install @ryuzaki13/react-foundation-lib
78
+ ```
79
+
80
+ Используйте только опубликованный package subpath `/formulas`:
81
+
82
+ ```ts
83
+ import {
84
+ compileTableFormula,
85
+ configureTableFormulaRegistry,
86
+ createTableFormulaRegistry,
87
+ executeTableFormula,
88
+ getTableFormulaById,
89
+ getTableFormulaList,
90
+ validateTableFormulaDependencies
91
+ } from "@ryuzaki13/react-foundation-lib/formulas";
92
+
93
+ import type { TableFormulaDefinition, TableFormulaExecutionResult, TableFormulaRegistry } from "@ryuzaki13/react-foundation-lib/formulas";
94
+ ```
95
+
96
+ Корневой импорт пакета и private imports из исходников не поддерживаются:
97
+
98
+ ```ts
99
+ // Неправильно.
100
+ // import { executeTableFormula } from "@ryuzaki13/react-foundation-lib";
101
+ // import { executeTableFormula } from "@ryuzaki13/react-foundation-lib/src/formulas/execute";
102
+
103
+ // Правильно.
104
+ import { executeTableFormula } from "@ryuzaki13/react-foundation-lib/formulas";
105
+ ```
106
+
107
+ Публичный модуль не зависит от React, DOM или browser-only API. Его runtime можно использовать в browser, worker и Node.js.
108
+
109
+ ## Быстрый старт
110
+
111
+ ### 1. Объявите формулу
112
+
113
+ ```ts
114
+ import type { TableFormulaDefinition } from "@ryuzaki13/react-foundation-lib/formulas";
115
+
116
+ const sumFormula: TableFormulaDefinition = {
117
+ id: "sum",
118
+ name: "Сумма",
119
+ description: "Складывает две выбранные зависимости: $0 + $1.",
120
+ args: ["первое слагаемое", "второе слагаемое"],
121
+ keywords: ["сложение", "арифметика"],
122
+ fn: (ctx) => ctx.num(0) + ctx.num(1)
123
+ };
124
+ ```
125
+
126
+ ### 2. Создайте и установите реестр
127
+
128
+ ```ts
129
+ import { configureTableFormulaRegistry, createTableFormulaRegistry } from "@ryuzaki13/react-foundation-lib/formulas";
130
+
131
+ const registry = createTableFormulaRegistry([sumFormula]);
132
+
133
+ configureTableFormulaRegistry(registry);
134
+ ```
135
+
136
+ Это bootstrap-операция. Выполните её один раз до первого чтения, validation, compile или execute.
137
+
138
+ ### 3. Выполните формулу
139
+
140
+ ```ts
141
+ const result = executeTableFormula({
142
+ formulaId: "sum",
143
+ keys: ["planned", "actual"],
144
+ rowData: {
145
+ planned: 100,
146
+ actual: "25"
147
+ }
148
+ });
149
+
150
+ if (result.ok) {
151
+ console.log(result.value); // 125
152
+ } else {
153
+ console.error(result.reason);
154
+ }
155
+ ```
156
+
157
+ Порядок `keys` определяет смысл indexes:
158
+
159
+ ```text
160
+ ctx.num(0) → rowData.planned
161
+ ctx.num(1) → rowData.actual
162
+ ```
163
+
164
+ Формула не знает имена выбранных полей заранее. Она знает только позиции аргументов.
165
+
166
+ ## Основные понятия
167
+
168
+ ### Строка данных
169
+
170
+ ```ts
171
+ type TableFormulaRowData = Record<string, unknown>;
172
+ ```
173
+
174
+ Это обычный JavaScript-объект, где ключ — id поля, а значение пока считается `unknown`:
175
+
176
+ ```ts
177
+ const rowData = {
178
+ planned: 100,
179
+ actual: "25",
180
+ label: "План"
181
+ };
182
+ ```
183
+
184
+ Модуль не копирует и не валидирует всю строку. Он читает только keys, к которым обращается formula context.
185
+
186
+ ### Зависимости и keys
187
+
188
+ Dependencies — упорядоченный массив id полей:
189
+
190
+ ```ts
191
+ const keys = ["planned", "actual"];
192
+ ```
193
+
194
+ Позиция является частью контракта:
195
+
196
+ - `0` означает `planned`;
197
+ - `1` означает `actual`.
198
+
199
+ Если поменять элементы местами, формула получит другой смысл без TypeScript-ошибки:
200
+
201
+ ```ts
202
+ // Формула вычисляет $0 - $1.
203
+ keys: ["planned", "actual"]; // planned - actual
204
+ keys: ["actual", "planned"]; // actual - planned
205
+ ```
206
+
207
+ Названия `keys` и `dependencies` обозначают один и тот же ordered mapping в разных API:
208
+
209
+ - `compileTableFormula` и `executeTableFormula` принимают `keys`;
210
+ - `validateTableFormulaDependencies` принимает `dependencies`;
211
+ - formatter runtime field обычно хранит `formulaDependencies`.
212
+
213
+ ### Реестр
214
+
215
+ Registry связывает стабильный `formulaId` с definition-функцией:
216
+
217
+ ```text
218
+ "sum" → sumFormula
219
+ "ratio" → ratioFormula
220
+ ```
221
+
222
+ Поскольку function не сериализуется, сохранённая конфигурация должна содержать id, а приложение при запуске обязано установить реестр с соответствующим definition.
223
+
224
+ ## Описание формулы
225
+
226
+ ```ts
227
+ interface TableFormulaDefinition {
228
+ id: string;
229
+ name: string;
230
+ description: string;
231
+ args?: readonly string[];
232
+ keywords?: readonly string[];
233
+ fn: TableFormulaFn;
234
+ }
235
+ ```
236
+
237
+ ### `id`
238
+
239
+ Устойчивый технический идентификатор:
240
+
241
+ ```ts
242
+ id: "share-of-total-percent";
243
+ ```
244
+
245
+ Он:
246
+
247
+ - trim-ится при создании реестра;
248
+ - обязан быть непустым;
249
+ - обязан быть уникальным после trim;
250
+ - может сохраняться в пользовательских конфигурациях;
251
+ - должен оставаться стабильным после релиза.
252
+
253
+ Не используйте display name как id и не генерируйте случайный id при каждом запуске.
254
+
255
+ ### `name`
256
+
257
+ Короткое название для каталога или select:
258
+
259
+ ```ts
260
+ name: "Процентная доля";
261
+ ```
262
+
263
+ Factory удаляет внешние пробелы, но не запрещает пустое name. За качество пользовательского каталога отвечает host.
264
+
265
+ ### `description`
266
+
267
+ Объясняет формулу и порядок зависимостей:
268
+
269
+ ```ts
270
+ description: "100 * $0 / ($0 + $1)";
271
+ ```
272
+
273
+ Factory trim-ит строку, но не анализирует выражение. `$0`, `$1` — только принятая человеком нотация документации, а не parser syntax.
274
+
275
+ ### `args`
276
+
277
+ Необязательные подписи positional arguments:
278
+
279
+ ```ts
280
+ args: ["выбранный показатель", "остальная часть"];
281
+ ```
282
+
283
+ Runtime не проверяет их количество и не связывает их с keys автоматически. Поле предназначено для редактора и документации.
284
+
285
+ Текущая factory сохраняет исходную ссылку на массив `args`; она не клонирует и не замораживает сам массив. Передавайте стабильный readonly-массив и не мутируйте его после создания registry.
286
+
287
+ ### `keywords`
288
+
289
+ Необязательные поисковые слова каталога:
290
+
291
+ ```ts
292
+ keywords: ["процент", "доля"];
293
+ ```
294
+
295
+ Factory создаёт отдельную копию массива, но элементы не trim-ит, а сам вложенный массив глубоко не замораживает. Считайте metadata read-only.
296
+
297
+ ### `fn`
298
+
299
+ ```ts
300
+ type TableFormulaFn = (ctx: TableFormulaContext) => number;
301
+ ```
302
+
303
+ Функция обязана:
304
+
305
+ - работать синхронно;
306
+ - возвращать `number`;
307
+ - возвращать только конечный результат;
308
+ - быть детерминированной для одинакового контекста;
309
+ - не иметь побочных эффектов;
310
+ - самостоятельно обрабатывать деление на ноль и другие бизнес-границы.
311
+
312
+ ```ts
313
+ const ratioFormula: TableFormulaDefinition = {
314
+ id: "ratio",
315
+ name: "Отношение",
316
+ description: "$0 / $1",
317
+ args: ["делимое", "делитель"],
318
+ fn: (ctx) => {
319
+ const divisor = ctx.num(1);
320
+ return divisor === 0 ? 0 : ctx.num(0) / divisor;
321
+ }
322
+ };
323
+ ```
324
+
325
+ ## Контекст формулы
326
+
327
+ ```ts
328
+ interface TableFormulaContext {
329
+ key: (index: number) => string | undefined;
330
+ value: (index: number) => unknown;
331
+ num: (index: number) => number;
332
+ }
333
+ ```
334
+
335
+ ### `ctx.key(index)`
336
+
337
+ Возвращает id dependency по индексу:
338
+
339
+ ```ts
340
+ // keys = ["planned", "actual"]
341
+ ctx.key(0); // "planned"
342
+ ctx.key(1); // "actual"
343
+ ctx.key(2); // undefined
344
+ ctx.key(-1); // undefined
345
+ ctx.key(1.5); // undefined
346
+ ```
347
+
348
+ Допустим только целый неотрицательный index в пределах массива.
349
+
350
+ Пустая строка технически может вернуться из `key`, если она была передана в `keys`, но `value` и `num` считают пустой key отсутствующим. Используйте только непустые dependency ids.
351
+
352
+ ### `ctx.value(index)`
353
+
354
+ Возвращает исходное значение без преобразования:
355
+
356
+ ```ts
357
+ // rowData.actual === "25"
358
+ ctx.value(1); // "25"
359
+ ```
360
+
361
+ Для неправильного индекса, пустого key или отсутствующего свойства возвращается `undefined`.
362
+
363
+ Используйте `value`, когда нужно отличать отсутствующее значение от нуля или применять собственный parser:
364
+
365
+ ```ts
366
+ fn: (ctx) => {
367
+ const raw = ctx.value(0);
368
+ if (raw === null || raw === undefined || raw === "") return 0;
369
+
370
+ const value = Number(raw);
371
+ return Number.isFinite(value) ? value : 0;
372
+ };
373
+ ```
374
+
375
+ ### `ctx.num(index)`
376
+
377
+ Читает значение и применяет обычный `Number(value)`. Если результат не конечный, возвращает `0`.
378
+
379
+ ```ts
380
+ ctx.num(0) + ctx.num(1);
381
+ ```
382
+
383
+ Точная coercion-семантика описана в следующем разделе.
384
+
385
+ ### `createTableFormulaContext(args)`
386
+
387
+ Создаёт context вручную. Основные сценарии — unit-тест своей formula и validation infrastructure:
388
+
389
+ ```ts
390
+ import { createTableFormulaContext } from "@ryuzaki13/react-foundation-lib/formulas";
391
+
392
+ const context = createTableFormulaContext({
393
+ rowData: {
394
+ price: "12.5",
395
+ count: 4
396
+ },
397
+ keys: ["price", "count"]
398
+ });
399
+
400
+ context.value(0); // "12.5"
401
+ context.num(0); // 12.5
402
+ context.num(1); // 4
403
+ ```
404
+
405
+ `keys` копируется при создании context. Изменение исходного массива после вызова не меняет mapping. `rowData` не копируется: context читает переданный объект.
406
+
407
+ ### Instrumentation
408
+
409
+ `createTableFormulaContext` принимает необязательные callbacks для наблюдения за indexes:
410
+
411
+ ```ts
412
+ const usedIndexes: number[] = [];
413
+ const invalidIndexes: number[] = [];
414
+
415
+ const context = createTableFormulaContext({
416
+ rowData: { A: 10 },
417
+ keys: ["A"],
418
+ instrumentation: {
419
+ onReadIndex: (index) => usedIndexes.push(index),
420
+ onOutOfRangeIndex: (index) => invalidIndexes.push(index)
421
+ }
422
+ });
423
+
424
+ context.num(0); // usedIndexes получает 0
425
+ context.num(2); // usedIndexes и invalidIndexes получают 2
426
+ context.num(-1); // invalidIndexes получает -1
427
+ ```
428
+
429
+ Для отрицательного, дробного или другого неправильного index вызывается только `onOutOfRangeIndex`. Для целого неотрицательного index сначала вызывается `onReadIndex`, даже если index оказался больше последнего элемента, а затем — `onOutOfRangeIndex`.
430
+
431
+ Тип instrumentation отдельно не экспортируется по имени, но TypeScript проверяет объект непосредственно в аргументах функции.
432
+
433
+ ## Числовое преобразование `ctx.num`
434
+
435
+ `ctx.num` не использует `toFiniteNumber` из formatter-ов. Внутри выполняется:
436
+
437
+ ```ts
438
+ const normalized = Number(value);
439
+ return Number.isFinite(normalized) ? normalized : 0;
440
+ ```
441
+
442
+ Отсюда следует стандартная JavaScript-coercion:
443
+
444
+ | Исходное значение | `ctx.num(index)` | Причина |
445
+ | ----------------- | ---------------: | ---------------------------------------------- |
446
+ | `10` | `10` | уже конечный number |
447
+ | `"10"` | `10` | numeric string |
448
+ | `"12.5"` | `12.5` | точка поддерживается `Number` |
449
+ | `""` | `0` | `Number("") === 0` |
450
+ | `" "` | `0` | whitespace string становится нулём |
451
+ | `null` | `0` | `Number(null) === 0` |
452
+ | `undefined` | `0` | `Number(undefined)` даёт `NaN`, затем fallback |
453
+ | `true` | `1` | `Number(true) === 1` |
454
+ | `false` | `0` | `Number(false) === 0` |
455
+ | `"abc"` | `0` | `NaN`, затем fallback |
456
+ | `Infinity` | `0` | не finite |
457
+ | `"1 234,5"` | `0` | локализованная строка не поддержана `Number` |
458
+
459
+ Это удобно для простых числовых DTO, но не является строгой validation.
460
+
461
+ Если строка может содержать пробелы группировки или запятую, выберите явную policy:
462
+
463
+ ```ts
464
+ import { toFiniteNumber } from "@ryuzaki13/react-foundation-lib/formatters";
465
+ import type { TableFormulaDefinition } from "@ryuzaki13/react-foundation-lib/formulas";
466
+
467
+ const localizedNumberFormula: TableFormulaDefinition = {
468
+ id: "localized-value",
469
+ name: "Локализованное число",
470
+ description: "Читает первую зависимость через SAP-like parser.",
471
+ fn: (ctx) => toFiniteNumber(ctx.value(0)) ?? 0
472
+ };
473
+ ```
474
+
475
+ Не добавляйте parser автоматически во все formulas, если transport уже возвращает numbers. Выбор зависит от контракта данных host-приложения.
476
+
477
+ ## Создание и установка реестра
478
+
479
+ ### `createTableFormulaRegistry(definitions)`
480
+
481
+ ```ts
482
+ function createTableFormulaRegistry(definitions: readonly TableFormulaDefinition[]): TableFormulaRegistry;
483
+ ```
484
+
485
+ Factory проходит definitions в исходном порядке и для каждого элемента:
486
+
487
+ 1. trim-ит `id`;
488
+ 2. запрещает пустой id;
489
+ 3. запрещает повторный id после trim;
490
+ 4. trim-ит `name` и `description`;
491
+ 5. копирует `keywords`, если они заданы;
492
+ 6. создаёт новый поверхностно замороженный definition;
493
+ 7. добавляет его в ordered `list` и lookup `byId`.
494
+
495
+ ```ts
496
+ const registry = createTableFormulaRegistry([
497
+ {
498
+ id: " sum ",
499
+ name: " Сумма ",
500
+ description: " $0 + $1 ",
501
+ fn: (ctx) => ctx.num(0) + ctx.num(1)
502
+ }
503
+ ]);
504
+
505
+ registry.list[0].id; // "sum"
506
+ registry.list[0].name; // "Сумма"
507
+ registry.byId.get("sum") === registry.list[0]; // true
508
+ ```
509
+
510
+ Пустой или дублирующийся id приводит к fail-fast исключению:
511
+
512
+ ```ts
513
+ createTableFormulaRegistry([{ id: " ", name: "Пустая", description: "", fn: () => 0 }]);
514
+ // Error: Formula id не может быть пустым
515
+ ```
516
+
517
+ ```ts
518
+ createTableFormulaRegistry([
519
+ { id: "sum", name: "A", description: "", fn: () => 0 },
520
+ { id: " sum ", name: "B", description: "", fn: () => 0 }
521
+ ]);
522
+ // Error: дублирующийся formulaId
523
+ ```
524
+
525
+ ### Readonly и freeze
526
+
527
+ ```ts
528
+ type TableFormulaRegistry = Readonly<{
529
+ list: readonly TableFormulaDefinition[];
530
+ byId: ReadonlyMap<string, TableFormulaDefinition>;
531
+ }>;
532
+ ```
533
+
534
+ Registry object, list и каждый definition поверхностно заморожены. Это защищает основные metadata fields от обычной мутации.
535
+
536
+ Freeze не является глубоким:
537
+
538
+ - `args` сохраняет входную ссылку;
539
+ - `keywords` копируется, но сам массив не заморожен;
540
+ - `byId` типизирован как `ReadonlyMap`, однако runtime object остаётся обычным `Map`.
541
+
542
+ Не обходите readonly casts и не мутируйте внутренности. Считайте registry полностью неизменяемым consumer-контрактом.
543
+
544
+ ### `configureTableFormulaRegistry(registry)`
545
+
546
+ ```ts
547
+ configureTableFormulaRegistry(registry);
548
+ ```
549
+
550
+ Заменяет активный module-level registry. По умолчанию активен пустой реестр, поэтому без настройки любой formula id считается неизвестным.
551
+
552
+ Правильный lifecycle:
553
+
554
+ ```text
555
+ объявить definitions
556
+ ↓
557
+ createTableFormulaRegistry
558
+ ↓
559
+ configureTableFormulaRegistry
560
+ ↓
561
+ validate configs
562
+ ↓
563
+ compile runtime fields
564
+ ↓
565
+ execute для строк
566
+ ```
567
+
568
+ Устанавливайте registry в composition root до первого React render или другого consumer runtime.
569
+
570
+ ### Глобальность реестра
571
+
572
+ Активный registry один для загруженного экземпляра ESM-модуля. `configureTableFormulaRegistry` не объединяет catalogs, а полностью заменяет текущий.
573
+
574
+ Не переключайте registry:
575
+
576
+ - во время component render;
577
+ - для каждой таблицы;
578
+ - перед каждым execute;
579
+ - как способ хранить пользовательский state.
580
+
581
+ В тестах замена допустима, но состояние нужно восстанавливать между test cases.
582
+
583
+ ### Уже скомпилированные executors
584
+
585
+ `compileTableFormula` получает definition из активного registry и замыкает его функцию. Если после этого установить другой registry, ранее созданный executor продолжит выполнять старую formula function.
586
+
587
+ Новые compile/execute вызовы будут использовать новый активный registry.
588
+
589
+ ## Чтение активного реестра
590
+
591
+ ### `getTableFormulaList()`
592
+
593
+ ```ts
594
+ const formulas = getTableFormulaList();
595
+ ```
596
+
597
+ Возвращает тот же readonly list активного registry, а не новую копию. Порядок совпадает с порядком definitions, переданных factory.
598
+
599
+ Это удобно для UI-каталога:
600
+
601
+ ```ts
602
+ const options = getTableFormulaList().map((formula) => ({
603
+ value: formula.id,
604
+ label: formula.name,
605
+ description: formula.description
606
+ }));
607
+ ```
608
+
609
+ ### `getTableFormulaById(formulaId)`
610
+
611
+ Trim-ит id и возвращает definition либо `undefined`:
612
+
613
+ ```ts
614
+ getTableFormulaById(" sum "); // definition с id "sum"
615
+ getTableFormulaById(""); // undefined
616
+ getTableFormulaById(" "); // undefined
617
+ getTableFormulaById(undefined); // undefined
618
+ getTableFormulaById("unknown"); // undefined
619
+ ```
620
+
621
+ Возвращается объект из registry, не clone.
622
+
623
+ ## Выполнение формулы
624
+
625
+ ### `executeTableFormula(args)`
626
+
627
+ ```ts
628
+ const result = executeTableFormula({
629
+ formulaId: "ratio",
630
+ keys: ["amount", "count"],
631
+ rowData: row
632
+ });
633
+ ```
634
+
635
+ Функция сначала компилирует formula + keys, затем один раз выполняет полученный executor.
636
+
637
+ Результат — discriminated union:
638
+
639
+ ```ts
640
+ type TableFormulaExecutionResult =
641
+ | { ok: true; value: number }
642
+ | {
643
+ ok: false;
644
+ reason: "formula_not_found" | "invalid_result" | "runtime_error";
645
+ };
646
+ ```
647
+
648
+ Всегда проверяйте `ok` перед чтением `value`:
649
+
650
+ ```ts
651
+ const result = executeTableFormula(args);
652
+
653
+ if (result.ok) {
654
+ return result.value;
655
+ }
656
+
657
+ switch (result.reason) {
658
+ case "formula_not_found":
659
+ return "Формула больше не доступна";
660
+ case "invalid_result":
661
+ return "Формула вернула некорректное число";
662
+ case "runtime_error":
663
+ return "Не удалось выполнить формулу";
664
+ }
665
+ ```
666
+
667
+ ### `formula_not_found`
668
+
669
+ Возвращается, если:
670
+
671
+ - `formulaId` равен `undefined`;
672
+ - id пуст после trim;
673
+ - id отсутствует в активном registry;
674
+ - registry ещё не установлен.
675
+
676
+ ### `invalid_result`
677
+
678
+ Formula function выполнилась без исключения, но вернула не конечный number:
679
+
680
+ ```ts
681
+ fn: () => Infinity;
682
+ fn: () => NaN;
683
+ ```
684
+
685
+ Даже если через ошибочный cast функция вернула строку `"10"`, результат будет invalid: `Number.isFinite` не выполняет coercion.
686
+
687
+ ### `runtime_error`
688
+
689
+ Formula function выбросила исключение:
690
+
691
+ ```ts
692
+ fn: () => {
693
+ throw new Error("Ошибка расчёта");
694
+ };
695
+ ```
696
+
697
+ Исключение перехватывается. Публичный результат не содержит исходный `Error`, stack или message. Это безопасная runtime-граница, но не полноценная diagnostics-система. Тестируйте formulas отдельно, чтобы видеть настоящую причину до production.
698
+
699
+ ### Отсутствующие keys
700
+
701
+ Если `keys` не переданы, context имеет пустой mapping. Formula всё равно компилируется, но `ctx.num(index)` возвращает `0`:
702
+
703
+ ```ts
704
+ executeTableFormula({
705
+ formulaId: "sum",
706
+ rowData: { A: 10, B: 20 }
707
+ });
708
+ // sum($0, $1) успешно вернёт 0 + 0 === 0
709
+ ```
710
+
711
+ Это не ошибка execution API. Ошибку недостаточного количества dependencies должен обнаружить `validateTableFormulaDependencies` до compile.
712
+
713
+ ## Предварительная компиляция
714
+
715
+ ### `compileTableFormula(args)`
716
+
717
+ ```ts
718
+ const compiled = compileTableFormula({
719
+ formulaId: "ratio",
720
+ keys: ["amount", "count"]
721
+ });
722
+
723
+ if (!compiled.ok) {
724
+ // Единственная compile-причина — formula_not_found.
725
+ throw new Error("Формула не найдена");
726
+ }
727
+
728
+ const first = compiled.execute({ amount: 100, count: 20 });
729
+ const second = compiled.execute({ amount: 120, count: 40 });
730
+ ```
731
+
732
+ Compile result:
733
+
734
+ ```ts
735
+ type CompileResult = { ok: true; execute: TableFormulaCompiledExecutor } | { ok: false; reason: "formula_not_found" };
736
+ ```
737
+
738
+ `CompileResult` показан для понимания, но отдельное именованное публичное type export для него отсутствует. TypeScript выводит тип из возвращаемого значения.
739
+
740
+ ### Что фиксируется при compile
741
+
742
+ Compiler:
743
+
744
+ - находит formula definition один раз;
745
+ - копирует массив `keys`;
746
+ - создаёт один context object;
747
+ - возвращает executor `(rowData) => result`.
748
+
749
+ Изменение исходного массива keys после compile не влияет на executor. Замена активного registry также не меняет уже найденную formula function.
750
+
751
+ ### Что происходит при каждом execute
752
+
753
+ Executor:
754
+
755
+ 1. записывает ссылку на текущий `rowData` во внутренний context;
756
+ 2. синхронно вызывает formula function;
757
+ 3. проверяет `Number.isFinite(result)`;
758
+ 4. возвращает success или controlled error.
759
+
760
+ `rowData` не копируется.
761
+
762
+ ### Когда выбирать compile
763
+
764
+ Для одной проверки или редкого расчёта допустим `executeTableFormula`. Для таблицы с десятками/сотнями строк компилируйте один раз на поле и переиспользуйте executor:
765
+
766
+ ```ts
767
+ const compiled = compileTableFormula({ formulaId, keys });
768
+
769
+ if (compiled.ok) {
770
+ for (const row of rows) {
771
+ const result = compiled.execute(row);
772
+ // обработка результата
773
+ }
774
+ }
775
+ ```
776
+
777
+ `executeTableFormula` выполняет lookup и compile на каждом вызове, поэтому не является оптимальным горячим путём ячейки.
778
+
779
+ ### Синхронность и повторный вход
780
+
781
+ Compiled executor переиспользует один context с изменяемой внутренней ссылкой на `rowData`. Это безопасно для обычной синхронной формулы.
782
+
783
+ Не делайте formula `async`, не сохраняйте `ctx` во внешнем state и не запускайте тот же executor рекурсивно из formula. Такой код нарушает lifecycle переиспользуемого context.
784
+
785
+ ## Валидация зависимостей
786
+
787
+ ### `validateTableFormulaDependencies(args)`
788
+
789
+ ```ts
790
+ const validation = validateTableFormulaDependencies({
791
+ formulaId: "sum",
792
+ dependencies: ["planned", "actual"],
793
+ availableColumnIds: ["planned", "actual", "label"]
794
+ });
795
+ ```
796
+
797
+ Результат:
798
+
799
+ ```ts
800
+ interface TableFormulaDependenciesValidationResult {
801
+ ok: boolean;
802
+ errors: TableFormulaValidationMessage[];
803
+ warnings: TableFormulaValidationMessage[];
804
+ usage: {
805
+ usedDependencyIndexes: number[];
806
+ usedDependencyIds: string[];
807
+ requiredDependencyCount: number;
808
+ };
809
+ }
810
+ ```
811
+
812
+ `ok` становится `false`, только если есть errors. Warnings не запрещают применение config.
813
+
814
+ ### Что проверяется
815
+
816
+ Validator:
817
+
818
+ 1. ищет formula в активном registry;
819
+ 2. проверяет, что каждый dependency id входит в `availableColumnIds`;
820
+ 3. запускает formula на положительных synthetic values;
821
+ 4. запускает formula на отрицательных synthetic values;
822
+ 5. через instrumentation собирает прочитанные indexes;
823
+ 6. обнаруживает обращения за пределы dependencies;
824
+ 7. сообщает неиспользованные dependencies;
825
+ 8. сообщает отсутствие чтения dependencies;
826
+ 9. сообщает исключение на synthetic data.
827
+
828
+ ### Errors
829
+
830
+ | `code` | Условие |
831
+ | ------------------------------- | ----------------------------------------------------------- |
832
+ | `formula_not_found` | Formula id отсутствует в registry |
833
+ | `dependency_not_available` | Dependency id отсутствует среди доступных колонок |
834
+ | `dependency_index_out_of_range` | Formula прочитала index, для которого dependency не передан |
835
+
836
+ ### Warnings
837
+
838
+ | `code` | Условие |
839
+ | ----------------------------------- | ----------------------------------------------------------------- |
840
+ | `unused_dependencies` | Выбраны dependencies, которые formula не прочитала на test passes |
841
+ | `formula_does_not_use_dependencies` | Formula не прочитала ни одного index |
842
+ | `formula_runtime_error` | Formula выбросила исключение на synthetic data |
843
+
844
+ ### Usage
845
+
846
+ ```ts
847
+ validation.usage.usedDependencyIndexes; // например [0, 1]
848
+ validation.usage.usedDependencyIds; // например ["planned", "actual"]
849
+ validation.usage.requiredDependencyCount; // 2
850
+ ```
851
+
852
+ `requiredDependencyCount` равен `максимальный использованный index + 1`.
853
+
854
+ Это полезно редактору:
855
+
856
+ - показать, сколько arguments ожидает formula;
857
+ - подсветить лишние dependencies;
858
+ - объяснить missing index;
859
+ - построить diagnostics без анализа JavaScript source.
860
+
861
+ ### `availableColumnIds` нужно передавать явно
862
+
863
+ По умолчанию список доступных колонок пуст. Если передать dependencies, но не передать `availableColumnIds`, каждый dependency будет считаться недоступным:
864
+
865
+ ```ts
866
+ validateTableFormulaDependencies({
867
+ formulaId: "sum",
868
+ dependencies: ["A", "B"]
869
+ });
870
+ // dependency_not_available для A и B
871
+ ```
872
+
873
+ Если вы хотите проверить реальную конфигурацию колонки, передайте полный разрешённый набор field ids.
874
+
875
+ ### Validator выполняет formula function
876
+
877
+ Validation — не статический анализ. Она действительно вызывает `fn` как минимум два раза. Поэтому formula обязана быть чистой:
878
+
879
+ ```ts
880
+ // Неправильно: validation и каждый row execution меняют внешний state.
881
+ fn: (ctx) => {
882
+ counter += 1;
883
+ return ctx.num(0);
884
+ };
885
+ ```
886
+
887
+ Не выполняйте в formula:
888
+
889
+ - сетевые запросы;
890
+ - запись в store;
891
+ - DOM-операции;
892
+ - mutation `rowData`;
893
+ - logging на каждый вызов;
894
+ - генерацию случайных значений;
895
+ - чтение текущего времени как части результата.
896
+
897
+ ### Ограничения validation
898
+
899
+ Два synthetic passes повышают шанс пройти разные branches, но не доказывают корректность для всех данных.
900
+
901
+ Validator не гарантирует:
902
+
903
+ - выполнение каждой условной ветки;
904
+ - корректность business formula;
905
+ - отсутствие деления на ноль на реальных данных;
906
+ - конечный numeric result;
907
+ - корректность `args` metadata;
908
+ - уникальность/непустоту dependency ids;
909
+ - наличие fields в реальном `rowData` во время execute.
910
+
911
+ Особенно важно: validation игнорирует возвращаемое число и проверяет только runtime exception. Formula, возвращающая `Infinity`, может пройти dependency validation, но execution затем вернёт `invalid_result`.
912
+
913
+ Поэтому validator дополняет, а не заменяет unit-tests formula.
914
+
915
+ ### Условные dependencies
916
+
917
+ ```ts
918
+ fn: (ctx) => (ctx.num(0) > 0 ? ctx.num(1) : ctx.num(2));
919
+ ```
920
+
921
+ Положительный и отрицательный passes могут обнаружить indexes `1` и `2`, но сложные branches всё равно способны скрыть dependency. Документируйте полный positional contract в `args` и тестируйте все ветки самостоятельно.
922
+
923
+ ### Повторяющиеся ids
924
+
925
+ Validator не дедуплицирует dependencies. Если передать один field id в нескольких позициях, оба indexes будут читать одно и то же свойство rowData. Обычно это ошибка конфигурации, хотя технически она допустима.
926
+
927
+ ## Интеграция с formatter pipeline
928
+
929
+ Модуль `formatters/pipeline` умеет выполнять table formula до остальных шагов форматирования.
930
+
931
+ Минимальный runtime field:
932
+
933
+ ```ts
934
+ import {
935
+ compileFormattersPipelineRuntime,
936
+ formatPipelineDisplayValue,
937
+ type FormattersPipelineRuntimeField
938
+ } from "@ryuzaki13/react-foundation-lib/formatters";
939
+
940
+ const field: FormattersPipelineRuntimeField = {
941
+ id: "total",
942
+ role: "measure",
943
+ type: "decimal",
944
+ formulaId: "sum",
945
+ formulaDependencies: ["planned", "actual"],
946
+ purelyDerived: true,
947
+ emptyWhenZero: false
948
+ };
949
+
950
+ compileFormattersPipelineRuntime(field);
951
+
952
+ const display = formatPipelineDisplayValue({
953
+ field,
954
+ rawValue: undefined,
955
+ rowData: {
956
+ planned: 100,
957
+ actual: 25
958
+ },
959
+ rowKind: "plain"
960
+ });
961
+ ```
962
+
963
+ Runtime flow:
964
+
965
+ ```text
966
+ rawValue
967
+ ↓ table formula из /formulas
968
+ ↓ formatter pipeline steps
969
+ ↓ typed formatting / fallback
970
+ display result
971
+ ```
972
+
973
+ Важно:
974
+
975
+ - registry должен быть установлен до `compileFormattersPipelineRuntime`;
976
+ - поле должно содержать все `formulaDependencies` в `rowData`;
977
+ - pipeline compiler сохраняет formula executor в самом field object;
978
+ - invalid/missing formula не выбрасывается при display execution;
979
+ - при `purelyDerived: true` ошибка формулы даёт пустое значение;
980
+ - без `purelyDerived` ошибка откатывается к `rawValue`;
981
+ - `emptyWhenZero` может скрыть успешный нулевой source result.
982
+
983
+ Полный lifecycle и mutation-контракт runtime compiler описаны в [README formatter pipeline](../formatters/pipeline/README.mdx).
984
+
985
+ ### Зависимости должны попасть в данные строки
986
+
987
+ `formulaDependencies` — это не request builder. Сам модуль не добавляет fields в OData select и не загружает их.
988
+
989
+ Owner таблицы/query обязан:
990
+
991
+ 1. проверить dependency ids относительно доступных columns;
992
+ 2. включить нужные fields в серверный запрос или локальный row model;
993
+ 3. передать полный `rowData` formatter runtime.
994
+
995
+ Иначе `ctx.value` вернёт `undefined`, а `ctx.num` — `0`, что может выглядеть как успешный, но неверный расчёт.
996
+
997
+ ## Отличие от row-based formatter
998
+
999
+ В пакете есть похожий, но отдельный реестр `formatters/rowBased`.
1000
+
1001
+ | Свойство | Table formula (`/formulas`) | Row-based formatter (`/formatters`) |
1002
+ | ------------------ | ------------------------------------------------------ | ---------------------------------------------------- |
1003
+ | Основная задача | Рассчитать numeric value колонки | Подменить текущее значение внутри formatter pipeline |
1004
+ | Результат function | Только `number` | `unknown` |
1005
+ | Context | `key`, `value`, `num` | дополнительно `rawValue`, `rowData`, `columnId` |
1006
+ | Место выполнения | До formatter pipeline | Шаг `rowBasedOverride` внутри pipeline |
1007
+ | Конфиг поля | `formulaId`, `formulaDependencies` | Pipeline step с `formulaId`, `dependencyIds` |
1008
+ | Registry | `configureTableFormulaRegistry` | `configureRowBasedFormatterRegistry` |
1009
+ | Error result | `formula_not_found`, `invalid_result`, `runtime_error` | Управляется `fallbackToRaw` pipeline step |
1010
+
1011
+ Не используйте одинаковый id как доказательство, что formula автоматически доступна в обоих реестрах. Это независимые catalogs и APIs.
1012
+
1013
+ Простое правило:
1014
+
1015
+ - вычисляемая числовая колонка → table formula;
1016
+ - contextual display override внутри pipeline → row-based formatter.
1017
+
1018
+ ## Архитектура и безопасность
1019
+
1020
+ ### Где размещать definitions
1021
+
1022
+ Foundation-пакет предоставляет только механизм. Конкретные business formulas принадлежат host-приложению:
1023
+
1024
+ ```text
1025
+ app composition / registries
1026
+ ├─ formula definitions
1027
+ ├─ единый ordered catalog
1028
+ ├─ createTableFormulaRegistry
1029
+ └─ configureTableFormulaRegistry при bootstrap
1030
+ ```
1031
+
1032
+ Формулу, специфичную для одной feature/entity, не следует переносить в foundation только потому, что она использует общий runtime.
1033
+
1034
+ ### Почему нет formula strings
1035
+
1036
+ Конфигурация вида `"$0 + $1"` не исполняется как код. Это осознанная граница:
1037
+
1038
+ - нет `eval`;
1039
+ - нет dynamic code generation;
1040
+ - нет необходимости доверять тексту из БД;
1041
+ - TypeScript проверяет написанные definitions;
1042
+ - обычные unit-tests видят функцию напрямую.
1043
+
1044
+ `description` может содержать формулу для человека, но runtime выполняет только `fn`, заранее включённую разработчиком в registry.
1045
+
1046
+ ### Доверенный код всё равно требует дисциплины
1047
+
1048
+ Модуль перехватывает exceptions и invalid numbers, но не может отменить уже выполненный side effect. Если formula изменила store, отправила запрос или мутировала rowData, возврат `runtime_error` не откатит это действие.
1049
+
1050
+ Формулы должны быть чистыми вычислениями.
1051
+
1052
+ ### Стабильность ids
1053
+
1054
+ Если id сохраняется в конфигурации пользователя, его переименование разорвёт связь и даст `formula_not_found`. Миграцию сохранённых configs должен выполнять владелец config boundary. Не добавляйте silent alias в foundation без отдельного требования.
1055
+
1056
+ ## Тестирование
1057
+
1058
+ ### Изоляция глобального реестра
1059
+
1060
+ Registry не имеет отдельной reset-функции. Для теста установите нужный registry, а после теста верните пустой или application registry:
1061
+
1062
+ ```ts
1063
+ import { afterEach, beforeEach } from "vitest";
1064
+ import { configureTableFormulaRegistry, createTableFormulaRegistry } from "@ryuzaki13/react-foundation-lib/formulas";
1065
+
1066
+ const testRegistry = createTableFormulaRegistry([sumFormula]);
1067
+
1068
+ beforeEach(() => {
1069
+ configureTableFormulaRegistry(testRegistry);
1070
+ });
1071
+
1072
+ afterEach(() => {
1073
+ configureTableFormulaRegistry(createTableFormulaRegistry([]));
1074
+ });
1075
+ ```
1076
+
1077
+ Если тесты выполняются параллельно в одном module instance и меняют registry, централизуйте setup или избегайте concurrent cases.
1078
+
1079
+ ### Unit-test formula без registry
1080
+
1081
+ ```ts
1082
+ import { createTableFormulaContext } from "@ryuzaki13/react-foundation-lib/formulas";
1083
+
1084
+ const context = createTableFormulaContext({
1085
+ rowData: { A: 100, B: 25 },
1086
+ keys: ["A", "B"]
1087
+ });
1088
+
1089
+ expect(sumFormula.fn(context)).toBe(125);
1090
+ ```
1091
+
1092
+ Этот тест показывает настоящую thrown error и удобен для business cases.
1093
+
1094
+ ### Integration-test через public runtime
1095
+
1096
+ ```ts
1097
+ expect(
1098
+ executeTableFormula({
1099
+ formulaId: "sum",
1100
+ rowData: { A: 100, B: 25 },
1101
+ keys: ["A", "B"]
1102
+ })
1103
+ ).toEqual({ ok: true, value: 125 });
1104
+ ```
1105
+
1106
+ Так проверяются registry lookup, context mapping и result boundary.
1107
+
1108
+ ### Минимальная матрица
1109
+
1110
+ Для каждой нетривиальной formula проверяйте:
1111
+
1112
+ - обычные положительные values;
1113
+ - нули;
1114
+ - отрицательные values;
1115
+ - отсутствующие properties;
1116
+ - строки вместо numbers, если transport это допускает;
1117
+ - деление на ноль;
1118
+ - порядок dependencies;
1119
+ - каждый conditional branch;
1120
+ - конечность результата;
1121
+ - validation с недостаточными и лишними dependencies.
1122
+
1123
+ Для registry отдельно проверяйте уникальность ids и expected order каталога.
1124
+
1125
+ ## Производительность
1126
+
1127
+ ### Горячий путь таблицы
1128
+
1129
+ Главная оптимизация — предварительная компиляция:
1130
+
1131
+ ```text
1132
+ медленнее: executeTableFormula для каждой ячейки
1133
+ быстрее: compileTableFormula один раз → execute(rowData) для каждой строки
1134
+ ```
1135
+
1136
+ Compiled executor переиспользует context и не делает registry lookup на каждую строку.
1137
+
1138
+ Formatter pipeline уже использует этот подход: runtime field компилируется на construction-stage.
1139
+
1140
+ ### Внутренние benchmark helpers
1141
+
1142
+ В исходном каталоге существуют `runTableFormulaRuntimePerf` и `runTableFormulaV2PrecompilePerf`, но `src/formulas/index.ts` их не экспортирует. Они предназначены для внутреннего perf-теста пакета и не являются публичным API `@ryuzaki13/react-foundation-lib/formulas`.
1143
+
1144
+ Не делайте private import. Для измерения своего consumer-кода используйте собственный benchmark вокруг публичного `compileTableFormula`.
1145
+
1146
+ Абсолютные timings зависят от устройства и runtime; сравнивайте несколько прогретых запусков и относительную разницу.
1147
+
1148
+ ## Частые ошибки
1149
+
1150
+ ### Не установить registry до runtime
1151
+
1152
+ Результатом будет `formula_not_found`, даже если definition существует в файле приложения, но не был передан `configureTableFormulaRegistry`.
1153
+
1154
+ ### Перепутать порядок dependencies
1155
+
1156
+ Indexes не знают бизнес-названий. `[A, B]` и `[B, A]` — разные формулы для вычитания/деления.
1157
+
1158
+ ### Считать `ctx.num` строгим parser
1159
+
1160
+ `null`, пустая строка и `false` превращаются в `0`, а `true` — в `1`. Локализованное `"1 234,5"` также даёт `0`.
1161
+
1162
+ ### Считать missing property runtime-ошибкой
1163
+
1164
+ `ctx.value` возвращает `undefined`, а `ctx.num` — `0`. Поэтому неполный query может silently изменить результат. Проверяйте request dependencies у владельца таблицы.
1165
+
1166
+ ### Игнорировать execution result
1167
+
1168
+ ```ts
1169
+ // Неправильно: value отсутствует в error branch.
1170
+ // const value = executeTableFormula(args).value;
1171
+
1172
+ const result = executeTableFormula(args);
1173
+ const value = result.ok ? result.value : 0;
1174
+ ```
1175
+
1176
+ Fallback `0` в примере — решение consumer-а, а не рекомендация для всех бизнес-сценариев.
1177
+
1178
+ ### Ожидать, что validator не выполняет formula
1179
+
1180
+ Он вызывает function на synthetic data. Любой side effect произойдёт уже при открытии/проверке редактора.
1181
+
1182
+ ### Ожидать, что validation найдёт `Infinity`
1183
+
1184
+ Dependency validator не проверяет returned value. `compile/execute` проверяют и возвращают `invalid_result`.
1185
+
1186
+ ### Регистрировать formulas во время render
1187
+
1188
+ Это глобальная application configuration. Настраивайте её до render и compile runtime.
1189
+
1190
+ ### Использовать `async` formula
1191
+
1192
+ Promise не является конечным number и даст `invalid_result`; сохранённый context также не рассчитан на чтение после `await`.
1193
+
1194
+ ### Хранить function в JSON
1195
+
1196
+ Сохраняйте только `formulaId` и dependency ids. Function определяется доверенным application bundle.
1197
+
1198
+ ### Путать table formula и row-based formatter
1199
+
1200
+ Они используют разные registries, contexts и этапы runtime.
1201
+
1202
+ ## API-справка
1203
+
1204
+ ### Runtime functions
1205
+
1206
+ | API | Назначение | Важное поведение |
1207
+ | ---------------------------------- | -------------------------------- | ------------------------------------------------------- |
1208
+ | `createTableFormulaContext` | Создать context вручную | Копирует keys, но не rowData; `num` использует `Number` |
1209
+ | `createTableFormulaRegistry` | Собрать нормализованный registry | Бросает `Error` для пустого/повторного id |
1210
+ | `configureTableFormulaRegistry` | Установить активный registry | Полностью заменяет module-level registry |
1211
+ | `getTableFormulaList` | Получить ordered catalog | Возвращает тот же readonly list |
1212
+ | `getTableFormulaById` | Найти definition | Trim id; `undefined` для пустого/неизвестного |
1213
+ | `compileTableFormula` | Подготовить reusable executor | Фиксирует formula и копирует keys |
1214
+ | `executeTableFormula` | Compile + один execute | Возвращает controlled result, не бросает formula error |
1215
+ | `validateTableFormulaDependencies` | Проверить mapping dependencies | Дважды исполняет formula на synthetic data |
1216
+
1217
+ ### Публичные TypeScript-типы
1218
+
1219
+ | Тип | Назначение |
1220
+ | ------------------------------------------ | ------------------------------------------ |
1221
+ | `TableFormulaRowData` | Строка `Record<string, unknown>` |
1222
+ | `TableFormulaContext` | Методы `key`, `value`, `num` |
1223
+ | `TableFormulaFn` | Синхронная function `context → number` |
1224
+ | `TableFormulaDefinition` | Metadata и function одной formula |
1225
+ | `TableFormulaRegistry` | Readonly `list` + `byId` |
1226
+ | `TableFormulaExecutionResult` | Success/error результат execute |
1227
+ | `TableFormulaCompiledExecutor` | Переиспользуемый `(rowData) → result` |
1228
+ | `TableFormulaValidationMessage` | `{ code, message }` validation diagnostics |
1229
+ | `TableFormulaDependenciesValidationResult` | Errors, warnings и usage dependencies |
1230
+
1231
+ `ValidationCode`, instrumentation type и compile-result type не экспортируются как именованные публичные типы. Их конкретные значения доступны через публичные function signatures и unions, описанные выше.
1232
+
1233
+ ### Причины execution error
1234
+
1235
+ | Reason | Значение |
1236
+ | ------------------- | --------------------------------------- |
1237
+ | `formula_not_found` | Formula отсутствует в активном registry |
1238
+ | `invalid_result` | Function вернула не конечный number |
1239
+ | `runtime_error` | Function выбросила исключение |
1240
+
1241
+ ### Validation codes
1242
+
1243
+ | Code | Severity |
1244
+ | ----------------------------------- | -------- |
1245
+ | `formula_not_found` | error |
1246
+ | `dependency_index_out_of_range` | error |
1247
+ | `dependency_not_available` | error |
1248
+ | `unused_dependencies` | warning |
1249
+ | `formula_does_not_use_dependencies` | warning |
1250
+ | `formula_runtime_error` | warning |
1251
+
1252
+ ## Вопросы и ответы
1253
+
1254
+ ### Почему неизвестная formula не бросает исключение?
1255
+
1256
+ Сохранённая конфигурация может ссылаться на удалённый id. Runtime представляет это как данные `{ ok: false, reason: "formula_not_found" }`, чтобы owner таблицы выбрал безопасный fallback.
1257
+
1258
+ ### Почему formula с пустыми keys успешно вернула `0`?
1259
+
1260
+ `ctx.num` для отсутствующего index возвращает `0`. Execution проверяет result, а не полноту dependency mapping. Запускайте validation до compile.
1261
+
1262
+ ### Почему `"1 234,5"` стало нулём?
1263
+
1264
+ `ctx.num` использует встроенный `Number`, который не понимает такую локализованную запись. Читайте `ctx.value` и применяйте согласованный parser, если ваш transport действительно возвращает этот формат.
1265
+
1266
+ ### Можно ли добавить пользовательский formula text из БД?
1267
+
1268
+ Нет. Модуль намеренно исполняет только functions, заранее включённые разработчиком в registry. Для пользовательского expression language нужен отдельный безопасный parser, grammar, validation и execution policy.
1269
+
1270
+ ### Нужно ли компилировать каждую строку?
1271
+
1272
+ Нет. Компилируйте один раз для комбинации formula id + ordered keys и переиспользуйте executor для строк.
1273
+
1274
+ ### Можно ли менять registry после компиляции?
1275
+
1276
+ Можно технически, но существующие executors сохранят старую function, а новые получат новую. В приложении избегайте смешанного состояния: установите registry один раз при bootstrap.
1277
+
1278
+ ## Связанная документация
1279
+
1280
+ - [Обзор пакета](../../README.md)
1281
+ - [Formatter pipeline](../formatters/pipeline/README.mdx)
1282
+ - [Row-based formatter](../formatters/rowBased/README.mdx)
1283
+ - [Числовые formatter-ы](../formatters/number/README.mdx)