letopis 1.4.1 → 2.0.0

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 (113) hide show
  1. package/AGENT-CHEATSHEET.md +329 -278
  2. package/CHANGELOG.md +558 -1
  3. package/README.md +606 -1037
  4. package/dist/acl.d.ts +78 -1
  5. package/dist/acl.js +264 -13
  6. package/dist/admin.d.ts +10 -141
  7. package/dist/admin.js +24 -175
  8. package/dist/auth.d.ts +2 -2
  9. package/dist/auth.js +5 -15
  10. package/dist/cache.d.ts +8 -1
  11. package/dist/cache.js +33 -10
  12. package/dist/chain.d.ts +19 -41
  13. package/dist/chain.js +169 -201
  14. package/dist/cli.js +51 -88
  15. package/dist/demo/booking.d.ts +352 -36
  16. package/dist/describe.d.ts +47 -0
  17. package/dist/describe.js +195 -0
  18. package/dist/errors.d.ts +59 -13
  19. package/dist/errors.js +190 -29
  20. package/dist/index.d.ts +52 -71
  21. package/dist/index.js +245 -130
  22. package/dist/internal.d.ts +12 -0
  23. package/dist/internal.js +5 -0
  24. package/dist/jsonschema.js +18 -17
  25. package/dist/load.d.ts +20 -12
  26. package/dist/load.js +225 -186
  27. package/dist/mcp.d.ts +5 -7
  28. package/dist/mcp.js +171 -215
  29. package/dist/messages/access.d.ts +90 -0
  30. package/dist/messages/access.js +113 -0
  31. package/dist/messages/api.d.ts +171 -0
  32. package/dist/messages/api.js +215 -0
  33. package/dist/messages/boot.d.ts +201 -0
  34. package/dist/messages/boot.js +256 -0
  35. package/dist/messages/codes.d.ts +172 -0
  36. package/dist/messages/codes.js +173 -0
  37. package/dist/messages/index.d.ts +95 -0
  38. package/dist/messages/index.js +118 -0
  39. package/dist/messages/mcp.d.ts +306 -0
  40. package/dist/messages/mcp.js +387 -0
  41. package/dist/messages/model.d.ts +233 -0
  42. package/dist/messages/model.js +283 -0
  43. package/dist/messages/query.d.ts +131 -0
  44. package/dist/messages/query.js +158 -0
  45. package/dist/messages/sql-access.d.ts +70 -0
  46. package/dist/messages/sql-access.js +87 -0
  47. package/dist/messages/sql-core.d.ts +150 -0
  48. package/dist/messages/sql-core.js +181 -0
  49. package/dist/messages/sql-time.d.ts +71 -0
  50. package/dist/messages/sql-time.js +88 -0
  51. package/dist/messages/sql-write.d.ts +246 -0
  52. package/dist/messages/sql-write.js +270 -0
  53. package/dist/messages/time.d.ts +23 -0
  54. package/dist/messages/time.js +29 -0
  55. package/dist/messages/types.d.ts +27 -0
  56. package/dist/messages/types.js +5 -0
  57. package/dist/messages/write.d.ts +405 -0
  58. package/dist/messages/write.js +509 -0
  59. package/dist/model.d.ts +73 -60
  60. package/dist/model.js +26 -29
  61. package/dist/ops.d.ts +35 -0
  62. package/dist/ops.js +101 -0
  63. package/dist/pglite.d.ts +0 -9
  64. package/dist/pglite.js +3 -1
  65. package/dist/registry.d.ts +3 -19
  66. package/dist/registry.js +3 -16
  67. package/dist/signals.d.ts +5 -9
  68. package/dist/signals.js +5 -9
  69. package/dist/sql.d.ts +13 -6
  70. package/dist/sql.js +176 -83
  71. package/dist/sync.d.ts +9 -3
  72. package/dist/sync.js +51 -21
  73. package/dist/tx.d.ts +26 -1
  74. package/dist/tx.js +44 -14
  75. package/dist/typed.d.ts +240 -45
  76. package/dist/types.d.ts +23 -91
  77. package/dist/types.js +12 -14
  78. package/dist/up.d.ts +53 -32
  79. package/dist/up.js +184 -73
  80. package/dist/uuid.js +5 -5
  81. package/dist/validate.d.ts +13 -1
  82. package/dist/validate.js +61 -29
  83. package/dist/watch.d.ts +11 -10
  84. package/dist/watch.js +33 -70
  85. package/dist/write.d.ts +9 -16
  86. package/dist/write.js +118 -187
  87. package/llms.txt +22 -25
  88. package/package.json +8 -11
  89. package/sql/10-core.sql +7 -50
  90. package/sql/15-errors.sql +64 -10
  91. package/sql/20-context.sql +7 -6
  92. package/sql/30-validate.sql +35 -27
  93. package/sql/40-class.sql +20 -114
  94. package/sql/50-acl.sql +8 -13
  95. package/sql/60-write.sql +220 -667
  96. package/sql/70-read.sql +20 -16
  97. package/sql/80-auth.sql +24 -27
  98. package/sql/90-time.sql +70 -859
  99. package/sql/95-seed.system.sql +7 -14
  100. package/sql/99-revision.sql +2 -1
  101. package/sql/README.md +32 -24
  102. package/sql/migrate/README.md +14 -0
  103. package/AGENT-CHEATSHEET.en.md +0 -485
  104. package/MIGRATION.md +0 -190
  105. package/README.en.md +0 -2859
  106. package/dist/import.d.ts +0 -185
  107. package/dist/import.js +0 -820
  108. package/dist/project.d.ts +0 -350
  109. package/dist/project.js +0 -1224
  110. package/sql/75-history.sql +0 -358
  111. package/sql/88-derived.sql +0 -1235
  112. package/sql/migrate/0006-changed-paths.sql +0 -3
  113. package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/CHANGELOG.md CHANGED
@@ -1,11 +1,568 @@
1
1
  # Changelog
2
2
 
3
- Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver.
3
+ Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver. Что обещают патч, минор и мажор, — раздел
4
+ «Версии и совместимость» в README; шаг версии сверяет `npm run check:semver`. С 1.4.2 у правки ядра (библиотеки и SQL)
5
+ в конце пункта указан источник находки: «Источник: тест», «пример», «сервис», «ревью», «soak» или «внедрение».
6
+
7
+ ## [2.0.0] — 2026-11-03
8
+
9
+ План 2.0, редакция 2 (`reports/PLAN-LETOPIS-2.0.md`, §2.1): 1.5.0 не выпускается — его работа (этап 3: права, один
10
+ процесс, ошибки и агент) входит в 2.0.0; 2.0.0 — вырезка редких фич и строгий режим (этап 4).
11
+
12
+ **Совместимости с 1.x нет.** 2.0 ставится только заново: схему letopis 1.x `up()` и `connect()` отклоняют до любых
13
+ изменений, `up({ schema, fresh: true })` пересоздаёт её, данные 1.x не переносятся. Код на API 1.x правят по разделам
14
+ «Снято» и «Строгий режим» ниже. Накат схемы проверяется в пределах 2.x: следующий выпуск 2.x проверит накат с 2.0.
15
+
16
+ ### Снято (несовместимо)
17
+ - Английский язык: `README.en.md`, `AGENT-CHEATSHEET.en.md`, `llms-full.txt` и его генератор
18
+ `scripts/gen-llms-full.mjs` (с `npm run llms:full`), корневые `README.en.md` и `CONTRIBUTING.en.md`; английские тексты
19
+ ошибок и подсказок каталога `lib/src/messages` (записи — только шаблон `ru`, подсказка — строка; тип `Lang` больше не
20
+ экспортируется); опция `lang` у `connect()`, `up()` и `createMcpServer()`, второй аргумент `describeText(model,
21
+ { lang })` и флаг `--lang` у команд CLI; английские описания инструментов MCP, их параметров и инструкции сервера.
22
+ Тексты ошибок, справки и описаний — только на русском; `err.docs` ведёт в раздел «Ошибки» `README.md`. Ошибку базы с
23
+ ключом библиотека отдаёт с текстом, который собрала база, — тем же, что видит сырой SQL; ключ и параметры — в
24
+ `err.key` и `err.params`, как прежде. Таблицы операторов в README, шпаргалке и главе «Чтение» сайта обновляет
25
+ `npm run ops:tables` (`scripts/gen-ops-tables.mjs`), сверяет `check:docs` (сверка `ops` вместо `llms-full`);
26
+ `check:docs` больше не сверяет английские версии (разделы «Tests», «Reserved names», «Errors», структуру) и `en` у
27
+ записей каталога. `lib/llms.txt` — по-русски.
28
+ - Импорт из 0.21: команда `letopis import` с её флагами (`--from`, `--from-schema`, `--tenant`, `--partition`,
29
+ `--keyless`, `--rename`, `--update`), модуль `src/import.ts`, тест `import` и фикстура 0.21
30
+ (`test/fixtures/gen-v021.mjs`, `v021.sql`), бенч против 0.21 и импорт полигона (`bench/compare.bench.mjs`,
31
+ `bench/seed-021.mjs`, `bench/import-salon.mjs`, `npm run test:import-salon`, итоги `compare` и `import-salon`, серия
32
+ `021` в `bench/core-write.mjs`) и dev-зависимость `letopis-0`. Снят `MIGRATION.md`: переход с 0.21 больше не
33
+ описывается — глава «Переход с 0.21» README (следующие главы — на номер меньше), раздел шпаргалки и ограничения импорта
34
+ сняты, глава «Производительность» — без сравнения с 0.21. Эталоны 0.21 для тестов прав и слияния (`test/oracle/`)
35
+ остались.
36
+ - Охранники API, снятого в 1.0.0, и код ошибки `removed` (записи каталога `api.removed`, `api.removed_option`,
37
+ `api.removed_many`, `api.replacement_*`, `access.replacement_*`, `write.replacement_load_trust`,
38
+ `model.cli_trust_removed`; функция `removed()` и `REMOVED_IN`): экспорты `uuidv7` и `LETOPIS_NS`; опции `connect()`
39
+ `partition`, `enforceAccount`, `enforceAcl`, `container`, `image`, `dataDir`, `redisPort`, `version`
40
+ (`REMOVED_OPTIONS`); методы цепочки `run`, `execute`, `account` и батча `execute`; свойства корня `reloadSchema`,
41
+ `accounts`, `credentials`, `resources`, `rules`, `compact`; `db.auth.sessions`, `db.acl.reload`, `watch(cb)` и
42
+ `watch(класс, cb)`; `load({ trust })` и `letopis load --trust`. Снятые имена больше не зарезервированы: `account`,
43
+ `execute`, `compact` и другие — обычные имена классов и ролей; `run` остался — это метод батча (теперь в
44
+ `BATCH_METHODS`). Неизвестную опцию называет `checkOptions` с похожими именами, неизвестный флаг CLI отклоняет разбор
45
+ аргументов. Раздел `removed` контракта API и его сверка в `check:semver` («новый охранник — мажор») убраны: снятое
46
+ имя исчезает из экспортов и списков методов, и `check:semver` требует мажор по ним.
47
+ - `letopis check` — проверки кода до запуска на TypeScript Compiler API (`src/check.ts`, флаги `--models` и `--json`,
48
+ записи каталога `model.check_*`, тест `check`) — и необязательная peer-зависимость `typescript` (для сборки она
49
+ осталась dev-зависимостью). `letopis sync --check` остался: тест `cli` (бывший `check` без правил `letopis check`).
50
+ - `db.describe()`, экспорт `describeText`, тип `ModelDescription` и команда `letopis describe` (флаг `--format`):
51
+ описание модели с примерами цепочек осталось только в инструменте MCP `letopis_model` — внутренний модуль
52
+ `src/describe.ts`, его нет в `exports`; поле `methods` описания и подписи текста `time.describe_*` сняты. Тест
53
+ `describe-examples` стал `mcp-model`: цепочки `examples` из ответа `letopis_model`, а также метаданные классов с
54
+ наследованием и условия `anyOf` и `oneOf` — проверки описания из `telemetry`.
55
+ - Сброс `db.reset({ level, confirm, tenant })`: опция `up({ allowReset })` (и флаг `--allow-reset` у `db/apply.mjs`),
56
+ функции базы `reset` и `reset_mark`, коды ошибок `reset_disabled`
57
+ (LT019) и `confirm_mismatch` (LT020), эндпоинт прав `letopis.reset`, служебная запись журнала вида `reset` (класс
58
+ `#reset`) и служебные сигналы `*` и `session:*` — их слал только сброс (`session:<тег>` погашенной сессии остался;
59
+ `*` и `session:*` от посторонней роли — теперь просто нагрузка не по режиму); типы `ResetOptions` и `ResetResult`.
60
+ `maintain({ now })` пускала только установка с `allowReset`; сама опция снята вместе с прореживанием (ниже). Первая
61
+ заливка больше не считает «пустой» схему с
62
+ записями сброса. Тесты: `reset` снят, сброс в `cache`, `signals`, `initial-load` (схема заново — `install(…, { fresh:
63
+ true })`), `hash`, `verify`, `roles`, `security-final` и `scenario-salon` — тоже.
64
+ - Совместимость с 1.x: 2.0 ставится только заново, ревизия движка — 8. `up()` на схеме letopis 1.x — ревизии 7 и
65
+ ниже или без штампа ревизии в комментарии схемы — отказывает `invalid_data` (ключ каталога `boot.schema_1x`) до любых
66
+ изменений, в том числе с `upgrade: true`; подсказка — `up({ schema, fresh: true })`: схема пересоздаётся, данные 1.x
67
+ не переносятся. `connect()` к такой схеме теперь тоже отказывает — до входа по ключу (было предупреждение «обновите:
68
+ up({ upgrade: true })»). Сняты миграции 0006 (колонка `log.changed`) и 0007 (внешний ключ `secret.account`) — в
69
+ свежей установке их ставит `sql/10-core.sql`; каталог `sql/migrate/` остался для миграций 2.x, их накатывает
70
+ `up({ upgrade: true })` в пределах 2.x, правило имени — README каталога. Сняты тест `upgrade` с dev-зависимостями —
71
+ пакетами прежних выпусков 1.0.1, 1.1.0, 1.4.1 и 1.4.2 (`npm uninstall`) — и бенч против 1.0.1 с его итогами в
72
+ `bench/results/`. Из файлов ядра ушло то, что служило только накату со схем 1.x: `drop … if exists` прежних сигнатур
73
+ функций (`family_lock`, `links_shape`, `targets_bad`, `check_links`, `signal`, `move_row`, `verify`, `row_visible`,
74
+ `acl_ok`, `version_changes`, `version_leaves`, `issue_session`, `auth_issue_otp`, `login_otp`, `derived_clear`,
75
+ `agg_classes`, `projector_behind`, `maintain`, `watch_head`, `watch_read`), функций сброса `reset` и `reset_mark`,
76
+ индекса `log_class_at` и строкового триггера `entity_ai` (теперь `create or replace trigger`), пометка удалёнными
77
+ строк `LOCK` других аккаунтов при накате (их заводили креды 1.1). В библиотеке — запасные вызовы для схем 1.x: нет
78
+ `signal_mode()` — режим `class`, у `maintain()` нет третьего аргумента — прежний вызов. Предупреждение `connect()`
79
+ «функции схемы поставила версия до 1.4.0» (штамп без версии движка) стало «поставила неизвестная версия letopis»
80
+ (`boot.stamp_unknown` вместо `boot.stamp_before_140`). Правило «каждый выпуск проверяет накат с предыдущего»
81
+ действует с 2.0: следующий выпуск 2.x проверит накат с 2.0. Тест `up`: «план 2.0, 7.10 №5» (схема 1.x — отказ `up()`
82
+ и `connect()` с подсказкой, `fresh` проходит) и механизм наката миграций 2.x; тест 7.10 №3 (хаб с зарезервированным
83
+ именем переживает `up()` с предупреждением) перенесён из `upgrade` в `up` — хаб заводит роль владельца мимо проверки
84
+ базы, а не пакет 1.4.2; `engine-races` №3в — без пометки строк `LOCK` накатом.
85
+ - Агрегаты в базе и проекторы (план 1.1, §2.4). Агрегаты: опция класса `aggregate` (тип `AggregateDef` в
86
+ `letopis/model`, тип `AggregateInfo` и поле `aggregate` у `ClassInfo`), `db.rebuildAggregates()`, триггеры
87
+ `entity_total_ai`, `entity_total_au`, `entity_total_ad` и функции `agg_*`. Проекторы: `db.projector()` с типами
88
+ `Projector`, `ProjectorOptions`, `ProjectorChange`, `ProjectorOut`, `ProjectorRow`, `ProjectorReport`,
89
+ `ProjectorStatus`, `ProjectorFailure`, опция класса `derivedBy` (производные классы; поле `derivedBy` у `ClassInfo`),
90
+ чекпоинты — строки `secret` вида `PROJECTOR`, функции `projector_state`, `projector_advance`, `derived_*`,
91
+ `projector_unread` и параметр `p_leaving` у `watch_read`. Сняты файлы `sql/88-derived.sql`, `src/project.ts` и
92
+ раздел каталога сообщений `derived`. Коды ошибок `derived_class` (LT021), `aggregate_field` (LT022),
93
+ `projector_pending` (LT023), `rebuild_required` и `projector_busy`. Метасхема `Class` не принимает `derivedBy` и
94
+ `aggregate` (`invalid_data`); описание модели в `letopis_model` — без них. Вид креда `PROJECTOR` больше не служебный.
95
+ `targets_bad` больше не отличает цель производного класса, `class_admit` не берёт блокировку семейств классов-целей
96
+ концов (её брали только ради пометки `derivedBy`), `syncModels` упорядочивает модели только по родителям. Имена
97
+ `projector` и `rebuildAggregates` больше не зарезервированы. Тексты отказов: «класс меняет предков или алиас» (было
98
+ «предков, алиас или derivedBy»), пересоздание класса с историей строк — «с прежними parent, alias и history». Второй
99
+ пул `connect({ admin })` — для `load`, `maintain` и `syncModels`. Сняты тесты `projector` и `aggregate`,
100
+ атаки №33, №34, №38, №39, №44, №50, №52, №53, №55 и №57 (номера остальных прежние), случаи проектора и
101
+ `rebuildAggregates` в `disconnect`, `messages`, `one-process` и `watch-contract` (7.4 №5, №6), агрегатов — в
102
+ `initial-load` (7.4 №10) и `sync`; бенч `bench/derived.bench.mjs` с итогами `derived*`. README — без глав «Агрегаты в
103
+ базе», «Рецепт: баланс агрегатом», «Проекторы», «Рецепт: дневная выручка», «Проектор» и ограничений проекторов и
104
+ агрегатов, глава «Производительность» — без замеров производных данных; шпаргалка — без разделов о них и граблей
105
+ №19–20 (раздел «Агенты и MCP» — теперь 11). Агрегаты чтения (`sum`, `avg`, `min`, `max`, `countBy`) остались. Долгий
106
+ прогон `bench/soak.mjs` переписан без агрегата и проектора (ниже, «Обвязка и проверка»).
107
+ - `diff()` и `blame()` (план 1.1, §2.1): терминалы цепочки, типы `DiffResult`, `DiffChange`, `DiffOptions`,
108
+ `BlameResult`, `BlameField`, `BlameOptions`, файл `sql/75-history.sql` (функции `jsonb_changes`, `row_changes`,
109
+ `change_paths`, `row_leaves`, `version_paths`, `history_segments`, `diff_versions`, `blame`) и колонка журнала
110
+ `log.changed`; записи каталога `api.version_or_moment`, `api.version_number`, `api.moment`. Журнал выдан приложению
111
+ всеми колонками: `select * from log` под RLS работает. Инструмент MCP `letopis_history` отдаёт только версии:
112
+ `kind` стал необязательным и принимает только `versions`, параметров `from`, `to`, `at`, `limit` нет (записи
113
+ `mcp.history_param`, `mcp.blame_follow`); причину записи агента показывают версии. Тест `history` — только причина
114
+ изменения (7.2 №5 и №8), `mcp` №9 — причина в версиях; бенч
115
+ `bench/history.bench.mjs` с итогом снят. Имена `diff` и `blame` больше не зарезервированы.
116
+ - Политика хранения истории и прореживание журнала (§2.11): у класса `history` — только `true` или `false` (объект —
117
+ `invalid_data` метасхемы `Class`), тип `HistoryPolicy` (`letopis`, `letopis/model`), опция `up({ history })` и
118
+ подстановка `@HISTORY_DEFAULT@`, функции базы `history_default`, `history_policy`, `history_iv`, `history_horizon`,
119
+ `history_keep`, `thin_batch`, метки `thin`, `thin_now` и `default_policy` строки ключа, тексты отказов про ярусы
120
+ (`sql-core.history_*`, `sql-core.system_history_policy`). `maintain()` журнал больше не трогает: одной транзакцией
121
+ удаляет сессии, коды и строки-замки удалённых классов и проверяет очередь сигналов; опции `maintain({ now, batches,
122
+ limit })`, тип `MaintainOptions`, поля отчёта `thinned`, `batches` и `more`, флаг `--batches` у `letopis maintain`,
123
+ параметры `maintain()` базы и запись `sql-time.maintain_now` сняты. Обрезка истории: `db.trimHistory()`, функция базы
124
+ `trim_history` и служебная запись журнала вида `trim` (записи `sql-time.trim_*`). Код ошибки `cursor_expired` (LT016):
125
+ курсор подписки больше не устаревает — версии журнала стирает только удаление аккаунта, а оно `cursor_expired` не
126
+ давало; время в курсоре не нужно, курсор — `tx:seq[~снимок]`; параметр `p_at` у `watch_read`
127
+ снят. Отказ смены `history` без права на все строки — «класс … включает или выключает журнал (history)». Настройки
128
+ `autovacuum_*_scale_factor = 0.02` у журнала сняты вместе с прореживанием. Сняты тест `retention`, тесты `maintain`
129
+ 7.3 №4 (очистка в первой пачке), `time` 6.4 (обрезка), обрезка в `load` (7.6.1 №9), `security-final` и акте 14
130
+ `scenario-salon`; `engine-class` №17 и атака `engine-attacks` №43 — про `history: false`. Имя `trimHistory` больше
131
+ не зарезервировано.
132
+ - Хэши версий и `verify()`: колонки `hash` (генерируемая) и `prev_hash` у `entity` и `log`, функции `snapshot` и
133
+ `version_hash`, `verify()` базы (право `letopis.verify`), `db.verify()` с типами `VerifyResult` и `VerifyIssue`,
134
+ команда `letopis verify` (`--data`), опция `load({ verify })` и флаг `letopis load --verify` (записи каталога
135
+ `write.load_db_invalid`, `write.load_db_target`), записи `sql-time.verify_*`. Режим истории загрузчика пишет версии
136
+ вставками по 5000 строк в прежнем порядке (по номеру версии), без волн с хэшами. Сняты тесты `hash` и `verify`, атака
137
+ `engine-attacks` №19 (правку в обход триггеров находил `verify()`; номер заново не занимается); вместо `verify()` в
138
+ тестах гонок и сценариях журнал сверяет помощник обвязки `journalIssues` (номера версий подряд, строка — последняя
139
+ версия). Бенч `bench/gencols.bench.mjs` мерит только `link_ids`. Имя `verify` больше не зарезервировано.
140
+ - Смена ключа и класса, затирание полей и стирание истории объекта: глаголы цепочки `rekey()`, `reclass()`,
141
+ `anonymize()` и `purge()` (с полями строки ответа `moved`, `$versions` и `$purged`), `db.rekeyClass()`,
142
+ `versions({ follow })` и параметр `follow` у `letopis_history`; функции базы `rekey`, `rekey_class`, `reclass`,
143
+ `move_row`, `links_replace`, `purge` и `watch_leaving`, колонка `moved` у `entity` и `log` с индексом `log_moved`,
144
+ служебная запись журнала вида `purge`. Класс, ключ и id строки неизменны: смена класса правкой — сырым
145
+ `UPDATE … SET class`, в том числе у строк `Class` и строк прав, — `immutable_key` («класс строки … неизменен», запись
146
+ `sql-write.class_immutable`); объект другого класса или с другим ключом — новый объект. Коды ошибок `use_reclass`
147
+ (LT011) и `reclass_denied` (LT012) сняты, номера заново не заняты; `isolation_level` — только у удаления,
148
+ `not_deleted` — только у `restore`. Записи каталога `write.anonymize_*`, `write.rekey_*`, `write.not_deleted_purge`,
149
+ `sql-write.class_row_kind`, `sql-write.system_row_reclass`, `sql-write.reclass_*`, `sql-write.use_reclass`,
150
+ `sql-write.rekey_needs_all`, `sql-write.refs_*`, `sql-write.target_not_found`, `sql-write.row_write_denied`,
151
+ `sql-write.class_write_denied`, `sql-time.purge_*`, `sql-time.rekey_*`, `sql-time.no_delete_class`,
152
+ `sql-time.no_write_class` и `sql-time.refs_denied*` сняты. Подписка читает журнал одной функцией `watch_read`:
153
+ событий ухода строки из классов подписки сменой класса больше нет. Историю строк не стереть: класс, у которого она
154
+ есть, не удаляется (класс, который придётся удалять, заводят с `history: false`), алиас предка удалённых потомков с
155
+ историей строк не меняется; версии журнала стирает только удаление аккаунта (`db.auth.purgeAccount`). Тексты отказов
156
+ удаления и пересоздания класса — без совета стереть историю. Сняты тест `rekey`, тесты `time` 6.1 (через смену id) и
157
+ 6.3, `chain-write` 4.8 и обезличивание, `read-contract` 7.1 №1 и №2, `watch-contract` 7.4 №4, `signals` 7.3 №16,
158
+ гонка `engine-races` №5, атака `engine-attacks` №42 (номер заново не занимается), события ухода в `watch`, «находка 1»
159
+ о `rekeyClass` в `security-final`, акт 16 `scenario-salon` и случай `load` 7.6.1 №9 после стирания истории; без смены
160
+ ключа и класса — `write-contract` 7.2 №2, `cache` №14, акт 13 `scenario-salon`, `chain-api`. Атака №5 и новый тест
161
+ `engine-write` — смена класса правкой даёт `immutable_key` у строки без ключа, с ключом и строки `Class`;
162
+ `write-contract` 7.2 №4 переводит строку в другой класс от роли владельца. README — без глав «Сменить ключ или класс»
163
+ (вместо неё — «Ключ и класс неизменны»), «Обезличить» и «Стереть историю»; шпаргалка — без смены ключа,
164
+ обезличивания, стирания истории и `follow`. Имена `rekey`, `reclass`, `anonymize`, `purge` и `rekeyClass` больше не
165
+ зарезервированы.
166
+
167
+ ### Строгий режим (этап 4; несовместимо)
168
+ - Строгие типы цепочек (7.10 №1, №6). `connect({ models })` и `connect<Models>()` с объявлениями `letopis types` дают
169
+ `TypedDb` без индексных сигнатур: шаг от корня — только класс или алиас моделей и системных классов (встроенный набор
170
+ `SystemModels`: `Class`, `Account`, `member`, `Resource`, `rule`; своя модель с тем же именем — вместо встроенной);
171
+ переходы выводятся из концов, как в `resolveHop`; слоты — только роли записываемого класса: `set(id | строка |
172
+ цепочка)`, `add` и `remove` — у множественного конца, `unset` — у необязательного; поля фильтров, `or()` и агрегатов —
173
+ поля класса (`sum` и `avg` — числовые). `letopis types` выдаёт ещё `<Класс>Model` — имя, алиас, концы с классами
174
+ целей и предки — и список `Models`: строгие типы работают и без моделей Zod. Новый экспорт `loose(db)` — то же
175
+ подключение без строгих типов для имён, известных только во время выполнения; типы `SystemModels`, `OrOp`, `ChainRef`.
176
+ `entity()` и значение слота принимают любую цепочку (`ChainRef`), `createMcpServer` и `syncModels` — подключение со
177
+ строгими типами. Модели без литеральных имён (`AnyModel[]`) строгих типов не дают — подключение `Db`, как прежде.
178
+ `db.as()`, `db.begin()`, `db.batch()` и `entity()` отдают цепочки без строгих типов.
179
+ - `update()` и `delete({ confirm: true })`, а ни у одного шага нет фильтра, id, `tags()`, `owner()` или `limit()` (и
180
+ раньше в цепочке нет `create()` или `upsert()`, а у `update` — `data.id`), — `invalid_query` до записи (запись
181
+ каталога `write.no_filter`): в цепочке, в батче (`run()` не пишет ни одного плана) и в `db.begin()` (транзакция
182
+ цела). Все строки шага — опция `{ all: true }`; превью `delete()` — без условий. Раньше такая правка шла по всем
183
+ строкам шага (7.10 №2). `update(патч, { rev, all: true })` с несколькими целями — по-прежнему `rev_ambiguous`.
184
+ - Неизвестная опция `connect()`, `up()`, `db.load()`, `db.watch()` и `createMcpServer()` — `invalid_data` с похожими
185
+ именами (`api.unknown_option`, `err.params.similar`) до подключения и прочих проверок; в работе 1.5 (этап 3) было
186
+ предупреждение (7.10 №4). `db.load()` сверяет опции и до пула администратора `connect({ admin })`.
187
+ - Неизвестное поле фильтра (и внутри `or()`), `sort('data.…')` или агрегата — `invalid_query` с похожими именами
188
+ (`query.unknown_field`, `err.params.similar`; план 2.0, этап 3, п. 3): поле не описано ни в одном классе шага —
189
+ классе, его потомках (если шаг не `exact()`), у роли — классах её целей; в фильтре годится и роль конца. В работе 1.5
190
+ было предупреждение `console.warn` с хвостом «(в 2.0 — ошибка invalid_query)», один раз на имя за процесс; хвост и
191
+ опция `future` у `lwarn` сняты. Сверяется только первый ключ пути (путь внутрь поля-объекта — нет), как и прежде.
192
+ У строк системного класса `Class` поля — свойства метасхемы описания класса (`kind`, `alias`, `parent`, `fields` и
193
+ другие), а не описание самого `Class` с одним `name`: прежнее предупреждение на `Class({ kind })` было ложным, отказа
194
+ нет. План записи проверяет фильтры, `sort()` целей и агрегат до первого оператора (`precheckStep` в `precheckPlan`):
195
+ опечатка в фильтре цели не ломает `db.begin()`; чтение — до запроса.
196
+ - `after()` перед `count()` и агрегатами чтения `sum`, `avg`, `min`, `max`, `countBy` — `invalid_query`
197
+ (`api.after_not_paged`) до запроса; раньше курсор там молча не учитывался (7.10 №7).
198
+ - Хаб с зарезервированным именем (`RESERVED_CLASS_NAMES`) отклоняют `hub()` (`model.hub_name_reserved`), `syncModels`
199
+ — модель, собранную мимо `hub()` (`model.sync_hub_name_reserved`), — и база (`class_admit`,
200
+ `sql-write.hub_name_reserved`); связь с таким именем база отклоняла и раньше. Хаб, заведённый мимо проверки базы,
201
+ `up()` оставляет и называет в предупреждении (`boot.reserved_hubs`), а правку его описания база отклоняет (7.10 №3).
202
+ - Документы: README — раздел «Строгий режим» (глава 4, вместо «Типы цепочек»), правила `update` и удаления, курсор,
203
+ «Зарезервированные имена», «Ошибки» и «Ограничения»; шпаргалка и `llms.txt` — так же. Правка с `{ rev }` больше не
204
+ называется «строгим режимом». Неизвестное поле фильтра, `sort()` и агрегата — отказ (выше), README, шпаргалка и
205
+ `llms.txt` — так же.
206
+
207
+ ### Сервисы-примеры и сайт (на урезанном API)
208
+ - Сервисы `services/` переведены на 2.0 и снова пройдены в Chrome по `CHECK.md` (3 ноября 2026 года). wallet: баланс —
209
+ поле счёта с `min(0)`, операция — один план `Wallet(счёт).Movement().create(…).Wallet().update({ balance: inc(amount) })`
210
+ с причиной у обоих глаголов, перевод — две операции в `db.begin()` под замками в порядке id, нехватка средств —
211
+ `invalid_data` по `minimum`; выписка по месяцам — запрос `group by` в транзакции сессии вместо проектора; агрегат,
212
+ производный класс `Statement`, перестройка и `verify` сняты. documents: подписание без `reclass` — одна транзакция
213
+ `db.begin({ reason })` создаёт `Contract` с данными черновика и удаляет `Draft` (договор — новый объект: класс входит в
214
+ v5-id), лента версий — версии обоих id; надгробие подписанного черновика не попадает в корзину (`restore` — 409),
215
+ новый черновик с номером договора — 422; смена номера, `diff`/`blame`, ярусы, обрезка, обезличивание, стирание,
216
+ `verify` и `follow` сняты, остались версии с причинами, `asOf`, `withDeleted`, превью удаления, `restore`, первая
217
+ заливка архива и `maintain()`. helpdesk: «Удалить из компании» снимает только членство (без `anonymize`) — строки
218
+ человека в компании база передаёт компании. salon: перенос брони — одна транзакция под замками мастеров: бронь
219
+ перечитывается `forUpdate()`, новая бронь на новом слоте с теми же концами, прежняя удаляется (время начала в ключе,
220
+ ключ неизменен — у новой брони новый id); надгробие переноса узнаётся по причине версии, история — `versions()` одного
221
+ id без `follow`. Сценарии: wallet 21 из 21, documents 24 из 24, helpdesk 74 из 74, salon 27 из 27.
222
+ - Сайт `guide/`: 51 исполняемый пример, снимки перезаписаны. Сняты примеры `derived/01–04`,
223
+ `history/02-follow-rekey-reclass`, `history/03-retention-maintain-trim`, `ops/01-verify-corrupt`, `ops/02-reset` и
224
+ глава «Агрегаты и проекторы» (главы перенумерованы: 12 «Массовая загрузка» … 16 «Все нюансы»); переписаны
225
+ `history/01-versions-asof`, `delete/02-restore`, `write/04-key-class-immutable`, `errors/03-strict-mcp-errors`
226
+ (строгий режим), `ops/02-sql-cache-signals`, `ops/03-cli`, `watch/02-quiet-cursor` (курсор не устаревает); новый
227
+ `ops/01-maintain` — отчёт `{ skipped, sessions, codes, queue }`. Раздел «Строгий режим» в главе моделей, нюансы
228
+ строгого режима с отметкой «ловит код», глоссарий без английской колонки и терминов снятого, ER-схемы и справочник —
229
+ из `db.registry` вместо `db.describe()`; покрытие API — 323 из 323.
230
+
231
+ ### Обвязка и проверка
232
+ - Долгий прогон `bench/soak.mjs` (план 2.0, §7.7) — без агрегата, производного класса и проектора, ярусов с засевкой
233
+ старой истории, `trimHistory`, `maintain({ batches })` и `verify`. Нагрузка прежняя по составу: писатели — заказ
234
+ горячего магазина и приращение его суммы `inc(amount)` одним планом, `inc` горячих счётчиков, правка последнего
235
+ заказа; читатели (цепочки с `limit`, ключ, `asOf`); подписка надолго и «открыл-закрыл»; `maintain()` раз в минуту;
236
+ вход по паролю и по коду с выходом; `db.as()` по 40 арендаторам; заведение и удаление классов. В конце вместо
237
+ `verify({ data: true })` — сверка журнала запросом (номера версий подряд, строка — последняя версия; тот же запрос,
238
+ что `journalIssues` обвязки тестов). В итоге `soak.md` — применимые критерии §7.7 (1–4, 6 и сверка журнала вместо 5);
239
+ снятые (5 — `verify`, 7 — отставание проектора, прореживание под записью) — строкой «снят в 2.0.0».
240
+ - Глава «Производительность» README — на сериях `core-*` (точка А) и `chain-read`, без чисел полигона salon; числа в
241
+ «Когда нужен загрузчик» и «Ограничениях» — из `core-write`.
242
+ - Текст отказа `maintain()` внутри `db.begin()` (`time.maintain_in_tx`): «maintain() идёт своей транзакцией, не внутри
243
+ db.begin()» — было «пачками в своих транзакциях»: прореживания пачками больше нет.
244
+ - Итоговая сверка документов с кодом. README: опции `up()` (`login`, `migrationsDir`, поле результата `login`) и
245
+ `connect()` (`admin`), `watch({ batch })`, поле `id` у `acl.list()`, границы строгого режима (сужает шаг глагола и шаги
246
+ до него, пустой `tags([])` не сужает, в батче отказ — при `run()`, связь с зарезервированным именем отклоняет база,
247
+ строгие типы — только у подключения с моделями); без «проверки» у администраторского подключения, «до 1.5.0»,
248
+ «ревизии 7» и перехода масок `Resource` с 1.x. Шпаргалка: строгий режим строкой в «Модели за минуту», опции `up()`,
249
+ `connect()` и `load()` полностью, отчёт `maintain()`, класс с историей строк не удаляется, без события ухода из классов
250
+ подписки, «Грабли 2.0». `llms.txt`, `lib/sql/README.md` (2.0, `@OWNER_NAME@`), `CONTRIBUTING.md`; пометки «снято в
251
+ 2.0.0» в `BACKLOG.md` и `COMPARISON.md`.
252
+ - `docs/checkpoint-guide.md` — пометка: исторический снимок, часть примеров снята в 2.0.0, числа устарели. Итог
253
+ `npm run prerelease` — `docs/checkpoint-2.0.0.md`.
254
+
255
+ ### Этап 3 (бывший 1.5.0)
256
+
257
+ ### Добавлено
258
+ - Обёртка прав `db.acl` (7.9 №1): `grant({ role | everyone: true, read, write, delete, api, weight })`, `deny(…)`,
259
+ `revoke(…)`, `roles(аккаунт, роли)` и `list()` — те же строки `Resource`, `rule` и `member` текущего арендатора с
260
+ детерминированными alias (`role:staff:ACCOUNT`, `everyone:ACCOUNT`, `class:Product:READ`, `api:reports.*:API`),
261
+ ключ правила — пара группа и объект, вес по умолчанию 50 — сильнее «можно всё» от `createTenant`. Повтор ничего не
262
+ меняет; права на запись правил, RLS и триггеры проверяет база; внутри `db.begin()` — в её транзакции. `everyone:
263
+ true` вместо `role` — правило для всех аккаунтов арендатора; `revoke({ everyone: true, read: '*', write: '*',
264
+ delete: '*' })` снимает «можно всё» от `createTenant` — права только по ролям (агентский прогон: снять его обёрткой
265
+ было нельзя, только удалением строк `rule` по id). `list()` отдаёт `{ role, everyone, group, op, target, condition,
266
+ permission, weight, enabled }` (`enabled: false` — правило выключено, база его не применяет) и на администраторском
267
+ подключении — только правила арендатора сессии (роль без RLS видит строки всех арендаторов). Типы `AclRule`,
268
+ `AclRuleSpec`, `AclTarget`.
269
+ - Один процесс для малых приложений (7.9 №3, №4): `up({ login: { user, password } })` заводит роль приложения или
270
+ обновляет её пароль и выдаёт `letopis_app` — имя и пароль квотирует база, привилегированную роль `up()` не трогает,
271
+ строка подключения роли — `r.login.dsn` (тип `LoginOptions`); `connect({ dsn, admin, token })` открывает второй пул
272
+ (не больше двух соединений) при первой надобности — для `load`, `maintain` и `syncModels` с той же сессией,
273
+ цепочки и `db.sql` остаются в пуле приложения под RLS.
274
+ - Все тексты ошибок библиотеки и базы — ключи каталога `lib/src/messages`: около 500 мест; `check:docs` следит, чтобы
275
+ литеральных текстов не осталось. Отказ инструмента MCP отдаёт `hint` и `similar`. Отказы `up()` с подходящим кодом —
276
+ `LetopisError` (`invalid_data`, `admin_required`), а не обычный `Error`.
277
+ - Тексты `up()` — ключи каталога: ошибки и предупреждения `up()`, в том числе заведения арендатора `tenant`, и ход
278
+ установки в консоли. Отказы без подходящего кода (версия сервера, кодировка базы, срок ожидания, ревизия схемы новее
279
+ или старше пакета, нет пакета PGlite) остаются обычным `Error` с префиксом `letopis.up:`, текст — тоже из каталога.
280
+ На ключи каталога переведены и тексты для пользователя и агента: предупреждение `connect()` о штампе движка, хвост
281
+ «откатите транзакцию и повторите весь блок `db.begin()`» у 40P01 и 40001, отчёт `letopis sync` и пометка
282
+ `letopis types`, пояснения к данным в ответах MCP, первая строка отказа инструмента, подсказка `confirm` превью, окно
283
+ и поле формы подтверждения удаления.
284
+ - `letopis types --system` — объявления и для классов арендатора System: системных и классов сидов пакета (например,
285
+ `booking`); без флага, как прежде, — только классы арендатора сессии.
286
+ - Тест исправления 7.5 №3, у которого его не было: установка без ADMIN OPTION на готовую роль владельца —
287
+ `admin_required` (код 42501 подтверждён на PostgreSQL 18.6); без исправления он падает.
288
+ - Каталог сообщений (каркас): `lib/src/messages` — ключ `<раздел>.<имя>` → код ошибки, шаблон с параметрами `{имя}` и
289
+ подсказка; у каждого кода — заголовок и подсказка (`codes.ts`), из них же строится страница ошибок сайта. Русские
290
+ тексты прежние. У `LetopisError` новые поля: `hint` — что делать, `docs` — ссылка на строку
291
+ кода в разделе «Ошибки» README на GitHub по тегу версии, `key` и `params` — ключ и параметры. База кладёт ключ и
292
+ параметры в `DETAIL`: перегрузки `lt_fail(ключ, параметры, detail)` и `lt_raise(код, ключ, параметры, detail)`,
293
+ `lt_text()` и `message_catalog()` — разделы `sql-*` каталога подставляет обычный `up()`, без новой ревизии; сырой SQL
294
+ видит прежний русский текст. На ключи переведены образцы: неизвестный класс или роль, `ValidationError`,
295
+ `connect({ schema })`, поле `sort()`, `auth.switch()` у `db.as()`, а в базе — нет членства в арендаторе, данные не
296
+ проходят описание класса, последний владелец арендатора; затем — все остальные места (пункт выше и пункт об
297
+ остатке ниже). `check:docs` сверяет полноту каталога (`messages`) и литеральные тексты ошибок (`literals`; список
298
+ `PENDING_LITERALS` пуст). Код `deleted` загрузчика вошёл в контракт API.
299
+ - Каталог сообщений, остаток: ключи каталога и у нарушений данных из базы — запись (`validate_data`, `inc`),
300
+ ужесточение класса (`err.detail.rows[].problem`), загрузчик (строки классов с `pattern`). Нарушение базы несёт
301
+ ключ каталога и параметры (`key`, `params`): библиотека их убирает, форма прежняя — `{ path, keyword, message }`
302
+ и `problem`; сырой SQL видит прежний русский текст и рядом ключ. Повторяемый конфликт с ключом (`40001`, `40P01`)
303
+ остаётся ошибкой базы с тем же SQLSTATE — на нём держатся повторы. `check:docs` (`literals`) ловит в `lib/sql`
304
+ ещё `raise` с литеральным текстом и литерал в `problem`. Источник: ревью.
305
+ - Новый код `internal`: строка-замок семейства не создана или не найдена (`lock_row`, `family_lock`). Раньше — голый
306
+ `XX000` с русским текстом без префикса, теперь `letopis: internal: строка-замок … не создана` с ключом в `DETAIL`, а
307
+ в библиотеке — `LetopisError` с кодом `internal` и подсказкой «сообщите о ней». Голые
308
+ `XX000` самого PostgreSQL — как прежде. Источник: ревью.
309
+ - Похожие имена (7.9 №6): общий `similar()` (`errors.ts`, его берёт и `letopis/mcp`). Неизвестный класс или роль в
310
+ цепочке — `invalid_class`, к тексту дописаны похожие имена реестра («— похожие: Shop»), они же в
311
+ `err.params.similar`. Неизвестное поле фильтра, `sort()` или агрегата — предупреждение `console.warn` с похожими
312
+ именами, один раз на имя; неизвестная опция `connect()`, `up()`, `load()`, `watch()` и `createMcpServer()` — тоже было
313
+ предупреждение; в 2.0.0 и то и другое стало отказом (раздел «Строгий режим» выше).
314
+ - Примеры цепочек в описании модели (7.9 №9): у каждого класса в ответе инструмента MCP `letopis_model` — `examples`,
315
+ рабочие цепочки из реестра: `create` (поля ключа и обязательные поля с образцами значений, обязательные концы —
316
+ слотами; нет у абстрактного и системного класса), `find` (по ключу или обязательному полю) и
317
+ `neighbors` (по концам класса и к классам, чьи концы ведут на него); `'<id Класс>'` — место для id. Образец значения —
318
+ `examples` из JSON Schema поля, `const`, `default`, `enum`, иначе по типу и формату. Модель больше `maxBytes`
319
+ приходит сначала без `examples`, затем краткой. Тест `mcp-model` исполняет каждую цепочку на демо-схеме.
320
+ - Один словарь фильтров (7.9 №8): реестр операторов `OPERATORS` в `src/ops.ts` — имя фабрики в TypeScript, ключ
321
+ JSON-фильтра MCP, вид его значения и смысл. Из реестра строятся таблица `OPS` и список операторов в описании
322
+ `letopis_query`, таблица «TypeScript — MCP — смысл» в README (вместо прежней из семи строк) и в шпаргалке, в
323
+ справочнике сайта и в главе «Чтение» сайта (без колонки MCP, вместо ручной таблицы из семи строк); в абзаце о MCP
324
+ README и в описании `letopis_query` справочника вместо ручного списка ключей — ссылка на неё. Таблицы с реестром
325
+ сверяет `check:docs`. Тест `llms`: реестр совпадает с фабриками пакета, ответами `letopis_query` и таблицами README,
326
+ шпаргалки и главы «Чтение» сайта.
327
+ - `letopis sync --check` (план 2.0, этап 3, п. 8; 7.9 №10) — сухой прогон с кодом 1, если он нашёл изменения: шаг CI
328
+ перед `--apply`; вместе с `--apply` — справка и код 2. Тест `cli`.
329
+
330
+ ### Исправлено
331
+ - Кэш результатов копил счётчик на каждую погашенную сессию: сигнал `session:<тег>` заводил запись в карте счётчиков
332
+ кэша каждого подключения, даже если сессия через это подключение не читала. Процесс, который впускает и выпускает
333
+ пользователей (вход по паролю и по коду, `revokeAll`), рос на 0,7–0,9 МБ в час на подключение до предела в 100 000
334
+ записей. Теперь счётчик сессии заводит снимок запроса через кэш, а сигнал меняет только заведённые; ответы погашенной
335
+ сессии сбрасываются, как прежде (план 2.0, 7.7). Источник: soak.
336
+ - Валидатор на TypeScript (`compileValidator`, проверка до записи в цепочках и загрузчике) исполнял шаблон `pattern`
337
+ из описания класса — его задаёт арендатор — движком V8 на данных: шаблон с катастрофическим перебором (`^(a+)+$` на
338
+ строке из 40 «a» и «!») вешал процесс приложения на часы. Теперь `pattern`, а также `anyOf` и `oneOf` с ним в ветке,
339
+ валидатор только разбирает (неверный шаблон — по-прежнему `invalid_class`), а проверяет база (`like_regex`, движок
340
+ PostgreSQL без такого перебора): при записи цепочкой — триггер, нарушение — тот же `ValidationError` (`invalid_data`);
341
+ в `db.begin()` оно, как другие ошибки базы, обрывает транзакцию. Загрузчик пишет мимо триггеров, поэтому строки
342
+ классов с `pattern` он сверяет запросом к базе на пачку до записи — нарушение в отчёте с номером строки, как прежде;
343
+ у классов без `pattern` запросов на пачку по-прежнему нет. Источник: ревью.
344
+ - Администраторское подключение (`allowBypassRls: true`) с сессией пользователя без прав не могло записать правило:
345
+ строки `Resource` оно заводило (RLS на нём не действует), а `rule` со слотами `.Group.set()` и `.Object.set()` на
346
+ них и `db.acl.grant` отвечали `target_not_found`. База проверяла, видит ли цели концов актор сессии, хотя роль
347
+ подключения видит любую строку. Так было у арендатора без «можно всё» (`grantAll: false`) и после снятия этого
348
+ правила. Теперь видимость целей по правам актора проверяется только у роли сеанса под RLS; суперпользователь, роль с
349
+ `BYPASSRLS` и член роли-владельца её пропускают. Существование цели,
350
+ её арендатор и класс проверяются, как прежде. Функцию обновляет обычный `up()`, новой ревизии нет. Рецепт — раздел
351
+ README «Права только по ролям» (агентский прогон плана 2.0, этап 3, п. 9). Источник: внедрение.
352
+ - Валидатор на TypeScript проверял `format: 'email'` шаблоном, который на подобранной строке (много точек в домене
353
+ и `@` в конце) перебирает квадратично: строка в мегабайт занимала процесс на минуты. Теперь адрес длиннее 254
354
+ символов (предел адреса по RFC 5321) — нарушение `format` без шаблона. Проверка в базе прежняя: длину она не
355
+ ограничивает, а движок PostgreSQL такого перебора не делает. Источник: ревью.
356
+ - Тесты исправлений 1.4.2, у которых их не было: 7.4 №7 — новый файл `disconnect` (обрыв TCP до и после COMMIT и на
357
+ открытии транзакции, `pg_terminate_backend` процесса, ждущего блокировку, — в транзакции загрузчика, первой заливки и
358
+ при открытии `db.begin()`: промис отклонён ошибкой связи, необработанных ошибок в процессе нет, следующая операция
359
+ пула проходит); 7.5 №3 — `impersonation` (`auth.switch` при имперсонации сырым SQL — `LetopisError` `acl_denied`,
360
+ LT006, а не сырая ошибка LT000). Без исправлений тесты падают — проверено откатом.
361
+ - Подключение по ключу (`connect({ apiKey })`, ключ сервиса) один раз обменивало ключ на сессию со сроком 7 суток и
362
+ больше его не обменивало: после недели простоя процесс получал `no_session` на запись и навсегда, пока его не
363
+ перезапустят, а чтение под истёкшей сессией шло как анонимное и молча отдавало пустые ответы. Так было у `svc` всех
364
+ четырёх сервисов-примеров. Теперь после простоя дольше минуты подключение до операции продлевает сессию и узнаёт,
365
+ жива ли она: по ключу — обменивает ключ заново (один обмен на все ждущие вызовы), с тем же рабочим арендатором, что
366
+ выбрал `auth.switch()`, и повторяет операцию (`runTx`, `db.begin()`); по токену — `no_session` на любую операцию
367
+ вместо пустого чтения. Ошибка продления другого рода (реплика только для чтения) работе не мешает, как прежде;
368
+ `refreshSession: false` — без проверки. Источник: сервис.
369
+ - `up()` от установщика без суперпользователя (роль с `CREATEROLE` и правом `create` на базе — поддержанный случай)
370
+ падал на `ANALYZE` с 42501 «permission denied for schema»: таблицы принадлежат роли владельца, а установщик состоит в
371
+ ней без наследования прав. С правом `usage` на схему PostgreSQL пропускал бы таблицы с WARNING, а `analyzed` называл
372
+ бы их собранными. Теперь `ANALYZE` идёт транзакцией от роли владельца, как у загрузчика; в `analyzed` — только
373
+ таблицы, где статистика появилась, оставшиеся без неё — предупреждение в `warnings`. `up({ fresh: true })` от такого
374
+ установщика падал «must be owner of schema» — схему теперь удаляет роль владельца. Источник: ревью.
375
+ - `npm run prerelease -- --only=…` и `-- --skip=…` затирали таблицу итогов в `docs/checkpoint-<x.y.z>.md`: в ней
376
+ оставались только шаги последнего запуска, хотя после падения повторяют упавший шаг и следующие. Теперь таблица
377
+ копится: строки шагов запуска заменяются, строки остальных остаются из прежних прогонов с датой и коммитом своего
378
+ прогона (колонка «Прогон»), шапка перечисляет прогоны, чьи строки в таблице; таблица прежнего вида читается как один
379
+ прогон. Запуск без шагов итог не пишет. Тест — `prerelease` (план 2.0, 7.8 №3).
380
+ - `node scripts/gen-api-contract.mjs --check` (его зовёт `check:docs`) и `guide/scripts/gen-api.ts --check` сравнивали
381
+ файлы побайтно и на Windows с `core.autocrlf=true` были красными на актуальных `docs/api-contract.json` и страницах
382
+ справочника сайта (CRLF в рабочей копии). Теперь сравнение — по LF, а `gen-api-contract.mjs` пишет файл с концами
383
+ строк самого файла.
384
+ - Загрузчик проверял шаблоны `pattern` (их процесс не исполняет — ревью, ReDoS) запросом к базе с `class_row` и полным
385
+ `validate_data` на каждую строку, в режиме истории — на каждую версию: засевка полигона salon (980 180 строк-версий)
386
+ шла на 42 % медленнее 1.0.1, 40 % времени загрузки — эта проверка (замер против 1.0.1). Теперь одинаковые класс и данные
387
+ проверяются один раз, строки классов читаются один раз на пачку (`materialized`), а полный разбор `validate_data`
388
+ идёт только у строк, которых не пропустил общий `validator` класса (правил `extra` в нём нет, но сюда доходят строки,
389
+ прошедшие валидатор на TypeScript, — у него те же правила, кроме `pattern`). Нарушения и первые 100 строк отчёта —
390
+ прежние, шаблоны по-прежнему исполняет только база. Засевка — 5671–5682 строки/с против 6245–6613 у 1.0.1 (−9…−14 %
391
+ в двух прогонах, было −42 %). Источник: тест.
392
+ - Чтение по одному id (по ключу или по id) собиралось как `id = any ($n::uuid[])`. Общий план подготовленного запроса
393
+ для `= any` PostgreSQL считает на ~10 элементов, и при неудачной выборке ANALYZE запрос навсегда оставался на планах
394
+ под каждый вызов — 0,5–0,8 мс планирования на чтение; та же ловушка у 1.0.1. Теперь один id — `id = $n::uuid`: общий
395
+ план стоит как план вызова и выбирается всегда (−18 % на схеме, где запрос застревал). «+27,7 %» замера против 1.0.1
396
+ «чтение по ключу» (прогон 2 ноября) был перебросом кэша планов между сторонами, а не кодом. Источник: тест.
397
+ - Подписка, закрытая (`close()`, `return()`), пока шёл её запрос журнала, не завершала `for await`: `close()` будит только
398
+ ждущий опрос, а после запроса цикл засыпал до будильника, которого уже не будет — таймер снят, сигналы отписаны. Так
399
+ поток SSE сервисов-примеров не закрывался после выхода. Теперь перед ожиданием подписка проверяет, не закрыта ли она.
400
+ Тест — `watch-contract` (план 2.0, 7.4 №8: запрос держит блокировка журнала, пока подписку закрывают; без исправления
401
+ цикл висит). Источник: сервис.
402
+ - Сервисы-примеры после обхода в браузере (сценарии `CHECK.md` четырёх сервисов через chrome-devtools-mcp, 2 ноября;
403
+ ядро не менялось). В кошельке выписка после «Перестроить выписку» шла в порядке строк таблицы — теперь по месяцам
404
+ (новые сверху) и номерам счетов. Время версий в
405
+ салоне и кошельке («История брони», «Последние операции», «История баланса», проход проектора) — местное, как в
406
+ «Под капотом»: сервер отдаёт `at` целиком, а не подстроку UTC. В helpdesk помощник считал открытыми только `new` и
407
+ `open` (счётчик и очередь — ещё `pending`); обновление очереди по событию сворачивало догруженные «Показать ещё»
408
+ страницы — теперь `GET /api/tickets?limit=N` перечитывает столько строк, сколько показано; у поля ответа и флажка
409
+ заметки — `id` и `name`; после отключения второго фактора текст «все сессии аккаунта закрыты» (гашение креда, который
410
+ не ключ, закрывает все сессии аккаунта, кроме сессии сервиса), так же — страница helpdesk сайта. Диалоги четырёх
411
+ сервисов (`components/ui/dialog.tsx`) снимают фокус с открывшей кнопки до `aria-hidden` Base UI и возвращают его после
412
+ закрытия (`finalFocus`) — предупреждение Chrome «Blocked aria-hidden on an element because its descendant retained
413
+ focus». `CHECK.md`: проверка — на свежем стенде (`down -v`); в кошельке чужая транзакция держит горизонт не дольше
414
+ 30 с; в helpdesk запись «Под капотом» об отключении второго фактора остаётся в закрытой сессии. Сценарии —
415
+ проверки полного `at` (кошелёк, салон), порядка выписки и `limit` очереди; без исправлений они
416
+ падают — проверено откатом. Повторный обход: поток событий SSE подписывался через фасад сервиса и переживал выход —
417
+ вкладка получала события прежнего салона, а с cookie новой сессии того же браузера показывала её данные под прежней
418
+ шапкой; теперь веб-сессия закрывает свои потоки (`services/shared/sessions.mjs`, `events.mjs`), а интерфейсы при
419
+ обрыве потока заново спрашивают `/api/me` и, если вошёл другой или никто, перезагружают страницу (проверка сценария
420
+ салона «выход закрывает поток событий своей сессии», без исправления падает). Строки очереди helpdesk и реестра
421
+ «Документов» фокусируемы (`tabIndex`, Enter и пробел открывают карточку): карточка открывается с клавиатуры, фокус
422
+ после закрытия возвращается на строку. Уход со страницы (переход на `/logout`) закрывает поток сразу — перезагрузка по
423
+ обрыву потока больше не перебивает начатый переход.
424
+
425
+ ## [1.4.2] — 2026-11-01
426
+
427
+ План 2.0 (`reports/PLAN-LETOPIS-2.0.md`): правила версий, проверка перед выпуском, исправления.
428
+
429
+ ### Добавлено
430
+ - Правила версий — подраздел «Версии и совместимость» в README (обе версии): патч — только исправления без новой
431
+ ревизии движка и без новых отказов; минор — новые возможности и новая ревизия с `up({ upgrade: true })`; мажор —
432
+ удаление, переименование, смена сигнатур и новые отказы.
433
+ - `npm run check:semver` (`scripts/check-semver.mjs`): контракт API и ревизия движка текущего дерева против тега
434
+ прошлого выпуска — исчезло → мажор, появилось или выросла ревизия → минор, тексты ошибок → патч; шаг версии обязан
435
+ быть не меньше. Входит в `check:docs` (в CI checkout с полной историей) и в `release.yml` до публикации. Сборка
436
+ контракта вынесена в `scripts/contract.mjs`; в `docs/api-contract.json` появилось поле `codes` — коды ошибок.
437
+ - `npm run prerelease` (`scripts/prerelease.mjs`) — полная локальная проверка одной командой, строго по очереди:
438
+ typecheck, сверка документов, весь `npm test`, примеры сайта со снимками, ER-схемы, покрытие, сборка сайта,
439
+ песочница, сценарии четырёх сервисов, проверка пакета с живым прогоном. Первый красный шаг останавливает прогон;
440
+ итог — таблица в `docs/checkpoint-<x.y.z>.md`. Тесты в CI не гоняются (решение владельца 01.11.2026).
441
+ - Долгий прогон `npm run soak` (`bench/soak.mjs`, план 2.0, этап 2): смешанная нагрузка одного процесса на часы —
442
+ писатели с агрегатом и `inc`, читатели, подписки, проектор, `maintain()` с прореживанием под записью, вход и выход,
443
+ `db.as()`, заведение и удаление классов; замер раз в 5 минут, в конце — критерии §7.7 и `verify({ data: true })`.
444
+ Быстрый режим на 15 минут — шаг `soak` в `npm run prerelease`.
445
+ - Метрика стабилизации: у правки ядра в CHANGELOG — источник находки, `release-notes.mjs` считает их в теле релиза.
446
+ - Тест `upgrade` №3: накат схемы 1.4.1 (dev-зависимость `letopis-141`) обычным `up()` без миграций; правило — каждый
447
+ выпуск проверяет накат с предыдущего. Тесты `semver` и `prerelease` проверяют сами сценарии (план 2.0, 7.8).
448
+ - `CONTRIBUTING.md` (и английская версия), шаблон issue об ошибке, форма отчёта о внедрении
449
+ `docs/adoption/TEMPLATE.md`.
450
+
451
+ ### Исправлено
452
+ - Строки-замки удалённых классов не убирались: запись строки `Class` заводит две строки `LOCK` в `secret` (замок
453
+ семейства и его шлюз), а удаление класса их оставляло — приложение, которое заводит и удаляет классы на лету, копило
454
+ их без предела. Теперь `maintain()` убирает строки-замки семейств, чей корневой класс удалён (или удалён его
455
+ арендатор); строку, которую держит транзакция, — следующим вызовом; класс с тем же именем заводит их заново
456
+ (7.7 №8). Источник: soak.
457
+ - `maintain()` мог безвозвратно стереть историю: если класс System заводили позже одноимённого класса арендатора,
458
+ строки журнала арендатора попадали в отбор дважды, и промежуточные версии ярусов стирались. Теперь политику выбирают
459
+ прореживание, `verify` и срок курсора подписки так же, как класс строки: свой класс арендатора, иначе System
460
+ (план 2.0, 7.3 №1). Источник: ревью.
461
+ - История удалённого объекта стирается целиком и тогда, когда до `reclass` он был в классе без политики: версии
462
+ прежнего класса больше не остаются и не показывают объект живым в срезе (7.3 №2). Источник: ревью.
463
+ - Стирание истории удалённого ждёт `restore()`, начатый в другой транзакции, и после него историю не трогает: раньше
464
+ цепочка восстановленного объекта начиналась не с версии 1 (7.3 №3). Источник: ревью.
465
+ - `maintain()` чистит истёкшие сессии и коды один раз за вызов, в первой пачке, а не в каждой из пачек (до 100):
466
+ у функции базы `maintain` третий аргумент `p_cleanup`; схема, которую ещё не обновил `up()`, получает прежний
467
+ вызов (7.3 №4). Источник: ревью.
468
+
469
+ - Два `create` одного ключа в батче проходили молча: обе строки ответа были одной и той же строкой. Теперь повтор —
470
+ `exists`, и батч откатывается целиком (7.2 №1). Источник: ревью.
471
+ - Строки ответа `reclass`, `restore` и `rekey` — в порядке целей (`sort()` до глагола, иначе id), как обещает README;
472
+ в 1.4.1 так стало только у `update` и `anonymize` (7.2 №2). Источник: ревью.
473
+ - Цепочка с глаголом и читающим хвостом в значении слота (`.Cat.add(db.Cat().create(…).Parent())`) падала с
474
+ `TypeError`; вложенный план не проходил проверки до базы. Теперь работает, а модификатор сразу после её глагола —
475
+ `invalid_query`, как у терминала (7.2 №3). Источник: ревью.
476
+ - Кэш результатов отдавал общие объекты: правка `rows[0].data` у ответа `.cache()` портила следующие ответы. Теперь
477
+ кэш хранит и отдаёт копии (7.2 №5). Источник: ревью.
478
+
479
+ - Подписка без событий опрашивала журнал сериями по 40 коротких повторов на каждый тик таймера — около 4 запросов в
480
+ секунду вместо одного за `pollMs`. Теперь короткие повторы идут только после сигнала (7.4 №1). Источник: ревью.
481
+ - `close()` подключения не закрывал живые подписки: с чужим пулом (`connect({ sql })`) они опрашивали журнал и после
482
+ `close()`. Теперь подписки закрываются, ожидающий `next()` получает `done: true` (7.4 №8). Источник: ревью.
483
+
484
+ - `reclass` класса без ключа (id тот же) сигналил только новый класс: ответы кэша и подписки прежнего класса не
485
+ сбрасывались и отдавали строку, которая из него ушла. Теперь сигнал идёт по обоим классам, как у пути через
486
+ триггер (7.4 №4). Источник: ревью.
487
+
488
+ - `letopis types` падал на кириллическом имени класса (`invalid_class`, хотя такой идентификатор в TypeScript допустим),
489
+ а для класса без полей, класса с `anyOf`/`oneOf` и открытой схемы выдавал неверный TypeScript
490
+ (`export interface XData Record<string, unknown>`). Теперь имена — буквы Unicode, не-объекты — `export type`, ветки
491
+ `anyOf` из одних `required` у класса с полями — условие, а не форма (7.5 №1). Источник: ревью.
492
+ - `hub('X', { fields: z.object(…) })` без других ключей описания читался как короткая форма — класс с единственным
493
+ полем `fields`. Теперь это полная форма (7.5 №2). Источник: ревью.
494
+ - Понятные ошибки вместо сырых: `connect({ owner })` с ролью, которой нет, — `invalid_data`; `anonymize(['опечатка'])` —
495
+ «поля нет в классе»; `up()` на готовой роли владельца без ADMIN OPTION — объяснение, что нужно; `auth.switch` при
496
+ имперсонации сырым SQL — `acl_denied` из каталога, а не неизвестный код; `--trust` — `LetopisError` (7.5 №3).
497
+ Источник: ревью.
498
+ - CLI: `letopis <команда> --help` возвращает код 0, неизвестная команда — справку с кодом 2 ещё до подключения к базе
499
+ (7.5 №4). Источник: ревью.
500
+ - Сравнение имени проектора в `88-derived.sql` — той же коллацией, что индекс `secret_kind_ident` (7.5 №5). Источник:
501
+ ревью.
502
+
503
+ - Срез `asOf` по классу и по концам показывал объект в классе, из которого он уже ушёл (`reclass` класса без ключа
504
+ сохраняет id): последняя версия выбиралась среди версий классов шага. Теперь она выбирается среди всех версий id, и
505
+ класс проверяется у неё — как уже делала ветка по id; то же в `trimHistory` по классам (7.1 №1). Источник: ревью.
506
+ - `after()` в `versions()` молча не действовал: страница «после курсора» повторяла первую. Теперь курсор листает версии
507
+ в порядке `sort()`. Курсор версии несёт её `rev` (`cursorOf` кладёт его сам), поэтому версии одного объекта с равным
508
+ полем сортировки не теряются. Без `sort()` — `invalid_query`, как у `rows()`. `count()`, агрегаты, `diff()` и
509
+ `blame()` курсор не учитывают, README теперь это описывает (7.1 №4). Источник: ревью.
510
+ - `paths()` с `after()` терял пути с общим последним узлом: курсор помнил только этот узел, и следующая страница
511
+ начиналась после всех путей к нему. Теперь `cursorOf(путь, поле)` кладёт в курсор id всех узлов (`path`), а пути
512
+ упорядочены по ним после поля и последнего узла. Ключи объекта пути идут в порядке шагов, как в примере README;
513
+ прежде они шли в порядке jsonb — по длине имени (7.1 №3). Источник: ревью.
514
+ - `versions({ follow: true })`, `diff({ follow: true })` и `blame()` после `rekey` смешивали историю перенесённого
515
+ объекта с объектом, которого потом завели на прежнем id (тот же ключ): брались все версии всех связанных id.
516
+ Теперь история идёт по отрезкам между переносами — надгробие переноса кончает отрезок, первая версия на новом id
517
+ начинает (функция базы `history_segments`, обычный `up()`); у нового объекта своя история с его создания (7.1 №2).
518
+ Источник: ревью.
519
+ - `blame()` на каждом новом соединении первые пять вызовов шли с квадратичной сложностью: свой план функции базы
520
+ `version_paths` по фильтру видимости оценивал версии в 2 строки и искал предыдущую версию по одному id, проверяя
521
+ номер фильтром, — около 250 мс на 1000 версиях и секунды на 10 000. Теперь обе версии ищутся по `(id, rev)` на
522
+ каждую пару: 21 мс с первого вызова (`bench/results/history.json`; ошибку нашёл замер `bench/history.bench.mjs`
523
+ при проверке 7.1 №2). Источник: тест.
524
+ - Проектор не строил данных арендатора, который стал участником после первой проекции: изменения шли только с
525
+ курсора, строки, заведённые до вступления, в проекцию не попадали. Теперь чекпоинт хранит состав участников
526
+ (`tenants`), и новый участник получает начальную перестройку пачками (7.4 №5). Источник: ревью.
527
+ - Проектор на подключении без LISTEN не перечитывал состав участников: сигнала о новых классах нет, и арендатор,
528
+ вступивший после запуска, не участвовал вовсе. Теперь состав читается в начале `run()` и не реже раза в 5 с (7.4 №6).
529
+ Источник: ревью.
530
+ - Реестры классов `db.as()` копились без предела: процесс, который ходит от имени многих арендаторов, держал реестр
531
+ каждого и перечитывал их все на каждый сигнал классов. Теперь их не больше 1000 на подключение, давно не нужный
532
+ вытесняется; фасад с вытесненным реестром работает — реестр возвращается и перечитывается (7.4 №8). Источник: ревью.
533
+ - Загрузчик с `existing: 'update'` писал в строку другого класса, если у неё тот же id: объект класса без ключа после
534
+ `reclass` сохраняет id, и повторный импорт по `$ext` правил его данными, проверенными по схеме прежнего класса, мимо
535
+ триггеров. Теперь такая строка — `exists` с классом объекта в тексте (7.2 №4). Источник: ревью.
536
+
537
+ - Имя поля, в котором есть `@S@` (например, `a@S@b`), портилось в запросе цепочки: построитель подставлял на это место
538
+ имя схемы, и фильтр, `sort()` или агрегат по такому полю ничего не находили (7.1 №5). Источник: ревью.
539
+
540
+ - После обрыва соединения транзакция проектора, `db.begin()` при открытии и загрузчик слали `rollback` в мёртвое
541
+ соединение и возвращали его в пул — postgres.js 3.4 ронял процесс необработанным `TypeError`. Теперь, как в
542
+ `runTx`: после обрыва в соединение не пишут и в пул его не возвращают, промис отклоняется (7.4 №7). Источник: ревью.
543
+
544
+ - `watch({ from: 'start' })` после первой полной пачки событий старше яруса `all` падал с `cursor_expired`, хотя
545
+ обещан весь журнал. Теперь, пока подписка не догнала журнал, свои чтения она шлёт без срока курсора; курсоры
546
+ событий и `sub.cursor` время хранят, и продолжение с них после прореживания — по-прежнему `cursor_expired` (7.4 №3).
547
+ Источник: ревью.
548
+
549
+ - Подписка на класс не получала события его потомков, заведённых после `watch()`: список потомков фиксировался при
550
+ подписке, и курсор уходил за их события. Теперь классы с потомками берутся из текущего реестра (7.4 №2).
551
+ Источник: ревью.
552
+
553
+ ### Изменено
554
+ - CI: у задания `test` нет сервиса PostgreSQL (живой прогон пакета в CI выключен, база не нужна), комментарии
555
+ `ci.yml` описывают то, что задания делают.
4
556
 
5
557
  ## [1.4.1] — 2026-11-01
6
558
 
7
559
  Исправления, найденные прогоном примеров документации для людей (`guide/`).
8
560
 
561
+ По правилам версий, введённым в 1.4.2, часть изменений этого выпуска требовала минора (ревизия движка 7 с миграцией
562
+ `0007`, новый метод `auth.session`), а новые отказы (`invalid_query` для модификатора после глагола, `acl_denied` для
563
+ удаления `Account` System, код 1 у `letopis types` и `letopis describe` без сессии) и убранный код `not_implemented` —
564
+ мажора.
565
+
9
566
  ### Добавлено
10
567
  - `db.auth.session(token)` — сведения о сессии по её токену для сервиса: чья сессия (`account`), в каком арендаторе
11
568
  она работает (`tenant`), цело ли там членство (`member`; в собственном арендаторе — всегда `true`), когда она