letopis 1.4.2 → 3.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 (123) hide show
  1. package/AGENT-CHEATSHEET.md +516 -398
  2. package/CHANGELOG.md +739 -0
  3. package/LICENSE +75 -21
  4. package/LICENSE-COMMERCIAL.md +49 -0
  5. package/README.md +1312 -1190
  6. package/api-contract.json +381 -0
  7. package/dist/acl.d.ts +81 -1
  8. package/dist/acl.js +281 -13
  9. package/dist/admin.d.ts +11 -141
  10. package/dist/admin.js +28 -186
  11. package/dist/auth.d.ts +20 -5
  12. package/dist/auth.js +80 -21
  13. package/dist/cache.d.ts +8 -1
  14. package/dist/cache.js +30 -8
  15. package/dist/chain.d.ts +46 -48
  16. package/dist/chain.js +556 -223
  17. package/dist/cli.js +73 -96
  18. package/dist/demo/booking.d.ts +352 -36
  19. package/dist/describe.d.ts +47 -0
  20. package/dist/describe.js +195 -0
  21. package/dist/errors.d.ts +77 -13
  22. package/dist/errors.js +231 -30
  23. package/dist/index.d.ts +62 -74
  24. package/dist/index.js +312 -155
  25. package/dist/internal.d.ts +7 -0
  26. package/dist/jsonschema.js +18 -17
  27. package/dist/load.d.ts +29 -17
  28. package/dist/load.js +235 -190
  29. package/dist/log.d.ts +121 -0
  30. package/dist/log.js +633 -0
  31. package/dist/mcp.d.ts +5 -7
  32. package/dist/mcp.js +194 -225
  33. package/dist/messages/access.d.ts +90 -0
  34. package/dist/messages/access.js +113 -0
  35. package/dist/messages/api.d.ts +237 -0
  36. package/dist/messages/api.js +304 -0
  37. package/dist/messages/boot.d.ts +201 -0
  38. package/dist/messages/boot.js +256 -0
  39. package/dist/messages/codes.d.ts +172 -0
  40. package/dist/messages/codes.js +173 -0
  41. package/dist/messages/index.d.ts +98 -0
  42. package/dist/messages/index.js +121 -0
  43. package/dist/messages/log.d.ts +229 -0
  44. package/dist/messages/log.js +102 -0
  45. package/dist/messages/mcp.d.ts +306 -0
  46. package/dist/messages/mcp.js +387 -0
  47. package/dist/messages/model.d.ts +256 -0
  48. package/dist/messages/model.js +314 -0
  49. package/dist/messages/query.d.ts +141 -0
  50. package/dist/messages/query.js +171 -0
  51. package/dist/messages/sql-access.d.ts +70 -0
  52. package/dist/messages/sql-access.js +87 -0
  53. package/dist/messages/sql-core.d.ts +164 -0
  54. package/dist/messages/sql-core.js +198 -0
  55. package/dist/messages/sql-time.d.ts +86 -0
  56. package/dist/messages/sql-time.js +111 -0
  57. package/dist/messages/sql-write.d.ts +246 -0
  58. package/dist/messages/sql-write.js +270 -0
  59. package/dist/messages/time.d.ts +23 -0
  60. package/dist/messages/time.js +29 -0
  61. package/dist/messages/types.d.ts +27 -0
  62. package/dist/messages/types.js +5 -0
  63. package/dist/messages/write.d.ts +452 -0
  64. package/dist/messages/write.js +572 -0
  65. package/dist/model.d.ts +77 -60
  66. package/dist/model.js +43 -34
  67. package/dist/ops.d.ts +35 -0
  68. package/dist/ops.js +101 -0
  69. package/dist/pglite.d.ts +0 -9
  70. package/dist/pglite.js +3 -1
  71. package/dist/registry.d.ts +3 -19
  72. package/dist/registry.js +3 -16
  73. package/dist/signals.d.ts +1 -5
  74. package/dist/signals.js +1 -5
  75. package/dist/sql.d.ts +9 -1
  76. package/dist/sql.js +175 -107
  77. package/dist/sync.d.ts +10 -4
  78. package/dist/sync.js +90 -17
  79. package/dist/tx.d.ts +21 -2
  80. package/dist/tx.js +76 -13
  81. package/dist/typed.d.ts +252 -53
  82. package/dist/types.d.ts +93 -90
  83. package/dist/types.js +17 -14
  84. package/dist/up.d.ts +56 -35
  85. package/dist/up.js +229 -83
  86. package/dist/uuid.d.ts +8 -0
  87. package/dist/uuid.js +14 -6
  88. package/dist/validate.d.ts +13 -1
  89. package/dist/validate.js +82 -33
  90. package/dist/watch.d.ts +10 -16
  91. package/dist/watch.js +36 -77
  92. package/dist/write.d.ts +87 -17
  93. package/dist/write.js +441 -206
  94. package/llms.txt +48 -31
  95. package/package.json +13 -14
  96. package/sql/10-core.sql +7 -50
  97. package/sql/15-errors.sql +64 -10
  98. package/sql/20-context.sql +18 -9
  99. package/sql/30-validate.sql +79 -30
  100. package/sql/40-class.sql +41 -115
  101. package/sql/50-acl.sql +8 -13
  102. package/sql/60-write.sql +220 -676
  103. package/sql/70-read.sql +11 -13
  104. package/sql/80-auth.sql +109 -42
  105. package/sql/90-time.sql +108 -947
  106. package/sql/95-seed.system.sql +7 -14
  107. package/sql/99-revision.sql +6 -1
  108. package/sql/README.md +32 -24
  109. package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
  110. package/sql/migrate/0010-impersonation-no-account.sql +5 -0
  111. package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
  112. package/sql/migrate/README.md +18 -0
  113. package/AGENT-CHEATSHEET.en.md +0 -488
  114. package/MIGRATION.md +0 -190
  115. package/README.en.md +0 -2910
  116. package/dist/import.d.ts +0 -185
  117. package/dist/import.js +0 -820
  118. package/dist/project.d.ts +0 -359
  119. package/dist/project.js +0 -1258
  120. package/sql/75-history.sql +0 -436
  121. package/sql/88-derived.sql +0 -1236
  122. package/sql/migrate/0006-changed-paths.sql +0 -3
  123. package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/CHANGELOG.md CHANGED
@@ -4,6 +4,745 @@
4
4
  «Версии и совместимость» в README; шаг версии сверяет `npm run check:semver`. С 1.4.2 у правки ядра (библиотеки и SQL)
5
5
  в конце пункта указан источник находки: «Источник: тест», «пример», «сервис», «ревью», «soak» или «внедрение».
6
6
 
7
+ ## [3.0.0] — 2026-11-03
8
+
9
+ Мажор: безопасность по умолчанию и журнал событий. Удаление и восстановление без сверки с тем, что видел пользователь,
10
+ теперь отказ, а не молчаливое действие; `createTenant` проверяет свой первый аргумент; вместо голых `TypeError` и
11
+ вводящих в заблуждение отказов — ошибки letopis с подсказкой. Ревизия движка 9 → 11: схему 2.x обновляет
12
+ `up({ schema, upgrade: true })` (миграции `sql/migrate/0010-impersonation-no-account.sql` — новое тело `ctx()`, и
13
+ `sql/migrate/0011-key-lock.otp-enabled.sql` — новые тела функций входа по ключу, отзыва креда, OTP и TOTP; раздел
14
+ «Исправления»).
15
+
16
+ ### Переход на 3.0
17
+ Что ломается и как переписать:
18
+
19
+ - **`delete({ confirm: true })` без `expect`** — отказ `invalid_query`, ключ `write.confirm_without_expect`, ничего не
20
+ удалено. Удаляйте ровно показанное превью:
21
+
22
+ ```ts
23
+ const preview = await db.Item(id).delete().rows(); // показать пользователю
24
+ await db.Item(id).delete({ confirm: true, expect: preview }).rows();
25
+ ```
26
+
27
+ Где удаление без превью — осознанный замысел (служебная чистка, удаление, подтверждённое в окне без показа
28
+ каскада), — `delete({ confirm: true, all: true })`: удалить текущий состав целей без сверки (без фильтра — все
29
+ строки класса, как прежде `all: true`). Правило одно для цепочек, `db.begin()`, `db.batch()` и `db.as()`.
30
+ - **`restore()` без `expect`** — отказ `invalid_query`, ключ `write.restore_without_expect`, ничего не восстановлено, и
31
+ для явных id тоже (`db.Item(id).restore()`): между удалением и восстановлением объект могли вернуть, изменить и удалить
32
+ снова — `restore()` вернул бы надгробие, которого пользователь не видел. Передавайте надгробия:
33
+
34
+ ```ts
35
+ const gone = await db.Item(id).delete({ confirm: true, expect: preview }).rows();
36
+ await db.Item(id).restore({ expect: gone }).rows();
37
+ // или по корзине: const tomb = await db.Item(id).withDeleted().rows(); restore({ expect: tomb })
38
+ ```
39
+
40
+ Вернуть то, что можно, — `restore({ expect, partial: true })`; осознанно вернуть все удалённые цели шага без сверки —
41
+ `restore({ all: true })`. Предупреждение `write.restore_no_filter` снято: без `expect` и `all` теперь отказ.
42
+ - **`createTenant(admin, …)`** — `admin` обязан быть пулом `postgres()` (функция с `begin` и `unsafe`); строка DSN,
43
+ подключение `connect()` letopis, `undefined` или другой объект — отказ `invalid_query`, ключ
44
+ `api.create_tenant_admin` (раньше — `TypeError: sql.begin is not a function`). Имя схемы нормализуется как в
45
+ `connect()` и `up()`: `createTenant(admin, 'salon', …)` работает со схемой `v2.salon` (раньше — сырая ошибка
46
+ PostgreSQL `42P01`).
47
+ - Типы опций: `delete(opts?: DeleteOptions)` и `restore(opts: RestoreOptions)` (новые экспортируемые типы) —
48
+ `delete({ confirm: true })` без `expect` и `all`, а также `restore()` без аргумента TypeScript отклоняет при
49
+ компиляции. `confirm` — литерал `true`, а не `boolean`.
50
+ - `svc.as(id)` с id аккаунта, которого нет (не заводили или удалён), — `target_not_found`, ключ
51
+ `sql-core.impersonation_no_account` «аккаунт … не найден» (раньше — `acl_denied` «действовать от имени этого аккаунта
52
+ нельзя», как запрет прав). Запрет (System, сервис, системный администратор, выключенный аккаунт) — как прежде
53
+ `acl_denied`.
54
+ - Связь, переданная полем данных (`create({ …, Member: id })`), — `invalid_data` с новым ключом `write.link_in_data`
55
+ вместо `api.validation_failed_end`: текст сам называет ключи и синтаксис `.Роль.set(id)`. Ключ находит и имя класса
56
+ на конце, если роль называется иначе. Код ошибки прежний.
57
+ - `connect({ apiKey | service | token })` со значением `null`, `''` (или не строкой) — отказ `invalid_data`, ключ
58
+ `api.connect_empty_credential`, до подключения (раньше — подключение без сессии и `no_session` на первой записи;
59
+ частый случай — повторный `up()` вернул `serviceKey: null`). Опция со значением `undefined` — как прежде, опции нет.
60
+ Источник: ревью.
61
+
62
+ ### Логирование
63
+ - Журнал событий: `connect({ log, logLevel, redact })` и `up({ log, logLevel, redact })`. `log` — функция
64
+ `(event) => void` или объект с методами `debug`/`info`/`warn`/`error` (pino, `console`): вызов
65
+ `logger[level](event, event.event)`. `logLevel` — `'debug' | 'info' | 'warn' | 'error' | 'off'`, по умолчанию
66
+ `'info'`; ниже порога событие не собирается. `LETOPIS_LOG=<уровень>` — если `log` не передан, события пишутся
67
+ JSON-строками в stderr. CLI: `letopis <команда> --log[=уровень]`.
68
+ - Событие: `{ ts, level, event, op, ms, tenant, account, tx, chain, rows, code, key, detail }`, у ошибок ещё `cause`.
69
+ Секреты (пароли, токены, ключ сервиса, пароль в DSN, значения секретов) маскируются всегда; `redact: string[]` —
70
+ дополнительные имена полей. `onQuery` остаётся; каждый SQL — ещё и событие `sql` уровня `debug`.
71
+ - События: `connect`, `close`, `up`, `migrate`, `sync`, `read`, `cache.hit`, `cache.miss`, `write`, `delete.preview`,
72
+ `delete`, `restore`, `tx.begin`, `tx.commit`, `tx.rollback`, `tx.retry`, `auth.login`, `auth.logout`, `auth.revoke`,
73
+ `auth.switch`, `watch.start`, `watch.stop`, `signal`, `maintain`, `warning`, `error`, `sql`. Уровни: `write`,
74
+ `delete`, `restore`, `auth.*`, `up`, `migrate`, `sync`, `maintain` — `info`; `warning`, `tx.retry` — `warn`;
75
+ `error` — `error`, а у штатного отказа — `warn` (ниже); `read`, `cache.*`, `tx.begin`, `tx.commit`, `sql`,
76
+ `delete.preview` — `debug` (`tx.commit` транзакции с записью — `info`, ниже). Каталог событий —
77
+ `lib/src/messages/log.ts`, слой — `lib/src/log.ts`.
78
+ - Ошибка PostgreSQL в событии `error` (сама или причиной ошибки letopis, поле `cause`) — только SQLSTATE и имена объектов
79
+ схемы (`constraint`, `table`, `column`, `schema`, `datatype`, `routine`); её текст, `DETAIL`, `WHERE`, `HINT` и
80
+ внутренний запрос в журнал не пишутся: в них бывают значения параметров и данных («Key (email)=(…)»,
81
+ «invalid input syntax for type uuid: "…"»). У ошибки letopis параметры шаблона `value` и `text` в `msg` — `***`
82
+ (`query.bad_uuid`, `api.db_bad_value`, курсоры), у строк в `detail` (превью удаления `write.preview_changed`, снимки
83
+ `restore`) поле `data` — `***`. Сама ошибка, которую получает приложение, не меняется. Те же правила — у предупреждений
84
+ `time.maintain_failed` и `log.notify_failed`. Источник: ревью.
85
+ - Штатный отказ — событие `error` уровня `warn`: коды `exists`, `conflict`, `target_not_found`, `not_deleted`,
86
+ `delete_restricted`, `acl_denied`, `tenant_denied`, `no_session`, `duplicate`, `deleted`, `confirmation_required` и
87
+ `invalid_data` с нарушениями данных (`err.issues`); прочие — уровня `error`. Источник: ревью.
88
+ - Фиксация в журнале: в `db.begin()` события записи (`write`, `delete`, `restore`, `raw`, `acl.change`, `auth.*` с
89
+ записью) несут поле `tx`; `tx.commit` такой транзакции — уровня `info` с `detail.writes` (число операций записи),
90
+ без записи — `debug`; `tx.rollback` — `info` с `detail.writes`. Событие записи с `tx` фиксацию не доказывает —
91
+ доказывает `tx.commit` с тем же `tx`; вне `db.begin()` событие записи пишется после `COMMIT`. `tx.commit` у
92
+ `db.transaction(fn)` (сырой SQL) — всегда `info`. Источник: ревью.
93
+ - Событие `up` — `detail.schema` (фактическое имя схемы) и `detail.requested`, если имя передано без префикса версии
94
+ (`'salon'` → `'v2.salon'`); ответ `up()` — поле `schema`, как прежде. Источник: ревью.
95
+
96
+ ### Добавлено
97
+ - `delete({ confirm: true, all: true })` и `restore({ all: true })` — осознанное действие без сверки (см. «Переход на
98
+ 3.0»). Экспорт типов `DeleteOptions`, `RestoreOptions`.
99
+ - `syncModels` до записи проверяет имена ролей концов: имя роли одно на арендатора, и роль, занятая системным классом
100
+ (`User`, `Tenant` — класс `member`, ведут на `Account`; `Group`, `Object` — класс `rule`, ведут на `Resource`), может
101
+ вести только туда же. Иначе `invalid_class`, ключ `model.end_role_system` «имя роли Group в классе Item занято
102
+ системным классом rule (там Group ведёт на Resource) — выберите другое имя роли, например Team»; роль, которая в другом
103
+ классе арендатора ведёт на другой класс, — `model.end_role_taken` (раньше — только отказ базы
104
+ `sql-write.end_other_class` «конец Group в классе rule ведёт на другой класс»). Источник: ревью.
105
+ - `sort('code')` — имя поля данных без префикса — отказ с подсказкой «— может быть, data.code?» (ключ
106
+ `query.sort_field_data`); имя без `data.` не принимается: колонки `at`, `rev`, `id` и поле данных с тем же именем
107
+ разошлись бы. Источник: ревью.
108
+ - `api-contract.json` (машинный контракт API и ошибок) входит в пакет. Источник: ревью.
109
+
110
+ ### Изменено
111
+ - `versions()` — по-прежнему терминал (промис строк), но метод цепочки на его результате (`versions().rows()`,
112
+ `versions().sort(…)`) — `invalid_query`, ключ `api.versions_terminal`, с подсказкой «модификаторы — до
113
+ `versions()`» вместо `TypeError: … rows is not a function`. Источник: внедрение.
114
+ - Инструмент MCP `letopis_write` (op `delete`) удаляет с `expect` — превью, которое подтвердил человек; расхождение под
115
+ блокировкой — `conflict` (`write.preview_changed`), ничего не удалено. Параметры инструмента прежние.
116
+ - `db.acl.revoke()` и `db.acl.roles(account, null)` удаляют правила и членство как осознанное действие
117
+ (`{ confirm: true, all: true }`); поведение прежнее.
118
+ - Подсказки об удалённом арендаторе, пользователе и членстве в `createTenant` (`boot.tenant_deleted` и соседние) и кода
119
+ `not_deleted` называют `restore({ expect })` с надгробием из `withDeleted().rows()`.
120
+ - Текст кода `conflict` в каталоге перечисляет все случаи: `update` с `{ rev }` (`write.rev_conflict`),
121
+ `delete({ confirm: true, expect })` с изменившимся превью или исчезнувшими целями (`write.preview_changed`,
122
+ `detail.preview: []`), `restore({ expect })` объекта, который меняли после удаления (`write.restore_stale`).
123
+ Подсказка `acl_denied` называет `db.acl.grant` (класс `rule`). Источник: ревью.
124
+ - Описание пакета в `package.json` — по-русски; агенту — `AGENT-CHEATSHEET.md` и `llms.txt` в пакете, исходный код
125
+ доступен (PolyForm Noncommercial 1.0.0).
126
+
127
+ ### Удалено
128
+ - Ключ каталога `api.validation_failed_end` (заменён `write.link_in_data`) и предупреждение `write.restore_no_filter`.
129
+
130
+ ### Исправления
131
+ - Типы `hub(имя, поля)` и `link(имя, поля)` без третьего аргумента: короткая форма с полями и без опций проходит
132
+ проверку типов (раньше TypeScript выбирал перегрузку полного описания и отклонял поле как лишнее ключевое слово;
133
+ при выполнении форма работала). Источник: пример.
134
+ - Вход по ключу (`connect({ apiKey })`, `db.auth.verifyApiKey`, `db.auth.verifyKeySecret`) и отзыв ключа
135
+ (`db.auth.revokeCredential`) синхронизированы по строке ключа: вход берёт её `for share`, отзыв — `for update`. Раньше
136
+ сессия, открытая входом параллельно с отзывом, переживала отзыв: её токен оставался действующим. Теперь отзыв ждёт
137
+ фиксации такого входа и гасит его сессию, а вход после отзыва ключа не находит. Источник: ревью.
138
+ - Код OTP и фактор TOTP, отключённые `setFlags(id, { enabled: false })`, не принимаются: `verifyOtp` — `null`,
139
+ `verifyTotp` и вход с `totp` — отказ, `totpEnabled` — `false`, и для смены своих кредов отключённый TOTP не требуется.
140
+ Раньше проверка кодов флаг не учитывала, и верный код отключённого OTP выдавал сессию. Источник: ревью.
141
+ - `setFlags(id, { enabled: false })` гасит сессии отключённого креда (ключ — открытые им, прочие виды — сессии
142
+ аккаунта, как при отзыве) и берёт строку креда `for update`: вход по ключу, начатый параллельно, отключение дожидается
143
+ и гасит его сессию. Последний действующий ключ сервиса не отключается (`acl_denied`, как при отзыве). Раньше
144
+ сессии, выданные кредом до отключения, оставались действующими. Источник: ревью.
145
+ - Загрузчик сверяет цель вне загрузки со всеми ролями, где встречается её id: id объекта `Org` в ролях `OrgRef → Org`
146
+ и `StaffRef → Staff` — `target_not_found` по второй роли. Раньше проверялась только первая ссылка на id, неверная
147
+ вторая записывалась (триггеры загрузку не страхуют). Источник: ревью.
148
+ - Загрузчик: класс строки, удалённый между первым чтением реестра и блокировкой семейства, — отказ `invalid_class`
149
+ (`write.load_class_not_found`), ничего не записано. Раньше строки писались по прежнему описанию удалённого класса.
150
+ Источник: ревью.
151
+ - Слот со вложенной цепочкой с глаголом и читающим хвостом (`.OrgRef.set(db.Org(id).Ref().create(…).Org())`) получал
152
+ у непустого результата id `undefined` — конец не ставился. Теперь — id строк хвоста. Источник: ревью.
153
+ - Чтение семейства, где потомок переопределил тип поля (`Item.price` — число, `Gift.price` — строка `'free'`):
154
+ `sort('data.price')`, сравнения (`gt`, `between`, `inList` …) и агрегаты `min`, `max`, `sum`, `avg` больше не падают
155
+ ошибкой приведения. Если у классов шага тип поля разный, значение, которое не приводится к типу поля класса шага, —
156
+ `null`: при сортировке по возрастанию стоит после прочих, условию сравнения не отвечает, агрегат его пропускает;
157
+ приводимое (`'7'`) сравнивается как число. Курсор `after()` такой строки — как курсор `null`. Источник: ревью.
158
+ - `lostRules()` и `syncModels().lost` сообщают о проверках `z.object(…).refine()`, `.superRefine()`, `.check()` на
159
+ уровне объекта (путь — имя класса, `kind: 'refine'`); раньше они терялись молча. Источник: ревью.
160
+ - `paths()`: ключ повторного шага — первый свободный суффикс среди всех выданных ключей (алиасы `x`, `x_2`, `x` →
161
+ `x`, `x_2`, `x_3`). Раньше третий узел получал ключ `x_2` и затирал второй. Источник: ревью.
162
+
163
+ ## [2.1.0] — 2026-11-03
164
+
165
+ Правки по прогонам первых пользователей и агентов. Новые опции необязательны. Что меняется для работающего кода —
166
+ три места, каждое ниже отдельной строкой: **смысл `withDeleted()` не у последнего шага** (теперь — модификатор своего
167
+ шага), **ошибка PostgreSQL `22P02` приходит `LetopisError` с кодом `invalid_query`** (раньше — сырая ошибка
168
+ postgres.js, в том числе у `db.sql`) и **`syncModels` отклоняет преобразования Zod**. Прочие новые отказы — там, где
169
+ раньше была сырая ошибка PostgreSQL, `TypeError` или молчаливая ошибка (`db.begin(fn)` и неизвестная опция
170
+ `db.begin()`, гонка `delete({ confirm, expect })`). Ревизия движка 8 → 9: схему 2.0 обновляет
171
+ `up({ schema, upgrade: true })` (миграция `sql/migrate/0009-restore-rev.tenants-name.sql`).
172
+
173
+ ### Добавлено
174
+ - `restore({ expect })` — восстановить ровно то, что ушло удалением: `expect` — надгробия `{ id, rev }` из ответа
175
+ `delete()` или `withDeleted()`. Восстанавливаются только цели из `expect`, и только если последняя версия каждой — то
176
+ же надгробие; иначе ничего не восстановлено, а отказ называет причину (строка ниже). Номер версии база сверяет ещё
177
+ раз под блокировкой записи журнала: `restore(id, rev)` в SQL. Источник: внедрение.
178
+ - `restore({ expect, partial: true })` — вернуть удаление целиком, насколько можно: восстанавливаются цели, у которых
179
+ совпал `rev`, а остальные не роняют операцию и приходят в ответе строками `{ …строка, $action: 'skip', $why }`,
180
+ `$why` — `'restored'` (уже восстановлена), `'reissued'` (id занял новый объект с тем же ключом) или `'changed'`
181
+ (после показанного удаления её меняли: удалили снова и т. п.); строка-пропуск — последняя версия цели (живая или
182
+ надгробие). Проигранная гонка с другим `restore()` тоже становится пропуском. `partial` без `expect` — `invalid_query`
183
+ (`write.partial_without_expect`). Продолжение цепочки после `restore()` идёт только от восстановленных. Источник:
184
+ агенты.
185
+ - Отказы `restore()` — по смыслу, по записи журнала за последним надгробием цели (и в проверке до записи, и в базе под
186
+ блокировкой — проигравший гонку видит версию победителя):
187
+ - за надгробием `restore` — объект уже восстановлен: `not_deleted`, новый ключ `write.restore_already` (в базе
188
+ `sql-time.restore_already`);
189
+ - за надгробием `create` — id занял новый объект с тем же ключом (`create()` по ключу удалённого): `not_deleted`,
190
+ `write.restore_id_taken` (в базе `sql-time.restore_id_taken`);
191
+ - прочее (удалён снова с другим `rev`, не удалялся, гонка) — `conflict`, `write.restore_stale`.
192
+
193
+ У всех трёх `err.detail.ids` — id этого вида; с `expect` `err.detail.stale` — все цели, которые не восстановить:
194
+ `{ id, rev, expected, after, deleted, live, why }` (`rev` — последняя версия сейчас, `after` — вид записи журнала
195
+ сразу за `expected`, `null` — её нет; `deleted` — последняя версия — надгробие; `live` — объект живёт; `why` — как
196
+ `$why`). Несколько целей разного вида — `restore_stale`, если есть `changed`, иначе `restore_id_taken`, если есть
197
+ `reissued`. Источник: внедрение, агенты.
198
+ - `delete({ confirm: true, expect: превью })` — удалить ровно показанное: превью строится заново в транзакции удаления и
199
+ сравнивается с `expect` по `id`, `rev` и `$action`; расхождение — `conflict` (`write.preview_changed`, новое превью —
200
+ `err.detail.preview`). Атомарно под гонкой: всё замыкание каскада (строки превью с `$action: 'delete'`) блокируется
201
+ `for update` в порядке id, превью пересчитывается уже под блокировкой. Связь к удаляемой цели, вставленная раньше,
202
+ попадает в новое превью (`preview_changed`); вставленная позже ждёт удаления и получает `end_target_missing` —
203
+ непоказанное больше не уходит каскадом. Пустые цели: `expect: []` и целей нет — пустой ответ; `expect` непуст, а целей
204
+ нет (удалены, невидимы) — `conflict` с пустым `err.detail.preview`. `expect` без `confirm` — `invalid_query`.
205
+ Взаимоблокировку двух удалений повторяет операция из пула (`40P01`), в `db.begin()` — вызывающий. Источник:
206
+ внедрение.
207
+ - `db.auth.revoke(token)` — выход пользователя по токену его сессии (право `auth.session`, как у `session(token)`);
208
+ без аргумента — как прежде, выход подключения. Функция базы `auth_logout_token(text)`. Источник: внедрение.
209
+ - `db.auth.tenants()` — у арендатора ещё `name` (имя аккаунта арендатора) и `personal` (собственный арендатор).
210
+ Источник: внедрение.
211
+ - Путь через удалённый узел одной цепочкой: `db.Member(id).withDeleted().Loan().withDeleted()` — удалённые выдачи
212
+ удалённого участника (`withDeleted()` — модификатор своего шага, см. «Изменено»). Источник: внедрение.
213
+ - `versions()` шага с явными id отдаёт и историю удалённого объекта. Источник: внедрение.
214
+ - `connect({ schema: 'salon' })` — имя без точки дополняется до `v2.salon`, как у `up()`. Источник: агенты.
215
+ - Экспорт типа `Expected` (`{ id, rev, $action? }`) — строка `expect`.
216
+
217
+ ### Изменено
218
+ - **Изменение смысла: `withDeleted()` — модификатор своего шага, а не всей выборки.** У последнего шага — как прежде:
219
+ `db.Order(o).line().withDeleted()` — живые и удалённые строки живого заказа. Не у последнего — другой ответ:
220
+ - было: `db.Order(o).withDeleted().line()` — живые и удалённые **строки** живого заказа (флаг относился к последнему
221
+ шагу, где бы ни стоял);
222
+ - стало: тот же вызов — заказ с удалёнными, а **строки только живые**; на такой вызов выводится предупреждение
223
+ `api.with_deleted_step` (один раз на пару шагов).
224
+ - Код, который ставил `withDeleted()` раньше последнего шага, переносят в конец: `db.Order(o).line().withDeleted()`;
225
+ удалённые строки удалённого заказа — `db.Order(o).withDeleted().line().withDeleted()`. Опция `withDeleted`
226
+ инструмента MCP `letopis_query` — по-прежнему последнему шагу. В цепочке записи `withDeleted()` любого шага, кроме
227
+ шагов `restore()`, — `invalid_query`, как прежде у модификатора цепочки. Источник: внедрение.
228
+ - **Ошибка PostgreSQL `22P02` (значение не того типа) — `LetopisError` с кодом `invalid_query`.** Где бы она ни
229
+ возникла — в цепочке, в `db.sql`, `db.transaction(fn)` или `tr.sql` в `db.begin()`:
230
+ - было: сырая ошибка postgres.js, `err.code === '22P02'`, текст PostgreSQL (`invalid input syntax for type uuid:
231
+ "42"`);
232
+ - стало: `err instanceof LetopisError`, `err.code === 'invalid_query'`, `err.key === 'api.db_bad_value'`, текст —
233
+ `letopis: invalid_query: значение не того типа: invalid input syntax for type uuid: "42"`; исходная ошибка
234
+ PostgreSQL — в `err.cause` (`err.cause.code === '22P02'`).
235
+ - Проверку `err.code === '22P02'` меняют на `err.code === 'invalid_query'` (SQLSTATE — из `err.cause.code`). Неверный
236
+ id шага, фильтра `{ id }` или конца в цепочке до базы больше не доходит — строка о `query.bad_uuid` ниже. `40001` и `40P01`
237
+ приходят как прежде. Источник: агенты.
238
+ - Фильтр `update()`, суженный не только по id (данные, концы, `tags()`, `owner()`), перепроверяется: цели
239
+ блокируются (`for no key update`) и читаются заново, правятся только строки, которые после ожидания блокировки ещё
240
+ подходят. Без гонки — те же цели. Источник: внедрение.
241
+ - `syncModels` (и `letopis sync`) отклоняет преобразования Zod — `.trim()`, `.toLowerCase()`, `.transform()`,
242
+ `z.preprocess()`, `.pipe()`: база и валидатор их не исполняют, данные записались бы как есть (`invalid_class`,
243
+ `model.transform_unsupported`). `refine` по-прежнему в отчёте `lost`. Источник: агенты.
244
+ - Неверный id шага, фильтра `{ id }` или конца — `invalid_query` (`query.bad_uuid`) до запроса вместо сырой ошибки
245
+ PostgreSQL 22P02, `null` как id — тот же отказ вместо `TypeError`; id в любом виде, который принимает PostgreSQL,
246
+ проходит как прежде. Источник: агенты.
247
+ - Текст `invalid_data` называет первое нарушение («путь — сообщение») и число нарушений; сообщение нарушения
248
+ перечисления называет допустимые значения, лишнего ключа — сам ключ (`sql-core.issue_additional`,
249
+ `model.issue_additional`); лишний ключ — имя конца класса — подсказка «конец — из пути или `.Роль.set()`»
250
+ (`api.validation_failed_end`, в `update()` — до записи). У класса с правилами только для базы (`pattern`) ошибка до
251
+ записи несёт полный список нарушений — его даёт база (`validate_row`). Путь и ключевое слово нарушений не менялись.
252
+ Источник: агенты.
253
+ - Неизвестный шаг: подсказка о классах арендатора (`auth.tenants()` → `auth.switch()` / `db.as(…, { tenant })`); если в
254
+ реестре сессии нет ни одного класса арендатора — свой ключ `api.unknown_step_no_classes`; системные классы в
255
+ «похожие» не идут. `.length`, `.map()`, `[0]` и другие свойства массива у цепочки и её перебор — `invalid_query`
256
+ (`api.chain_not_rows`). Источник: агенты.
257
+ - Предупреждения (не отказы, `console.warn` один раз на место): шаг с аргументом `undefined`
258
+ (`api.step_undefined_filter`), шаг после `sort()` (`api.sort_not_last`), `restore()` без сужения и `expect`
259
+ (`write.restore_no_filter`), `withDeleted()` не у последнего шага (`api.with_deleted_step`). Источник: агенты.
260
+ - `undefined` в данных `create()` и `upsert()` отбрасывается, как в патче `update()`. Источник: агенты.
261
+ - `close()` у фасада `db.as()` ничего не закрывает: пул общий с корнем. Источник: агенты.
262
+ - Логин без крайних пробелов в `auth.setPassword()`, `auth.verifyPassword()` и `createTenant({ user })`; кред,
263
+ записанный раньше с пробелами, находится по логину как есть. Источник: агенты.
264
+ - Подсказка `no_session` неверного ключа сервиса: ключ перевыпускает `up({ fresh: true })`. Источник: агенты.
265
+ - `connect({ token })` с токеном, который не даёт сессии (погашен `auth.revoke`, истёк, неверен), — отклонённый промис
266
+ `no_session` (`api.token_not_active`), а не подключение, где первый же шаг давал `invalid_class` про личный арендатор.
267
+ Если сессия погасла после `connect()`, неизвестный шаг у фасада с токеном без арендатора сессии — тот же отказ, но
268
+ терминалом (отклонённым промисом), а не синхронным выбросом. Источник: внедрение.
269
+ - `db.begin(аргумент)` — только опции `{ reason }`: функция, строка, число, массив — `invalid_query`
270
+ (`api.begin_arg`, подсказка — форма `begin()` / `commit()` / `rollback()`); раньше `begin(fn)` функцию не вызывал,
271
+ а транзакция оставалась открытой. Неизвестная опция — `invalid_data` с похожими (`api.unknown_option`). Источник:
272
+ внедрение.
273
+ - Нарушение `minLength` / `maxLength` называет границу: «длина строки меньше {limit}» / «больше {limit}» (ключи
274
+ `model.issue_string_too_short`, `model.issue_string_too_long`, в базе `sql-core.issue_string_too_short`,
275
+ `sql-core.issue_string_too_long` вместо `*.issue_string_length`); путь и ключевое слово нарушения не менялись.
276
+ Источник: внедрение, агенты.
277
+ - `forUpdate()` с тем, что блокировать нельзя (`lock_unsupported`, `api.for_update_unsupported`), называет помеху —
278
+ терминал (`count()`, `paths()`, `versions()`, агрегат), `asOf()`, `withDeleted()`, `deep()` или глагол, — а подсказка
279
+ перечисляет допустимое и форму «прочитать с блокировкой, затем править». Источник: агенты.
280
+
281
+ ### Лицензия
282
+ - **Смена лицензии: MIT → PolyForm Noncommercial License 1.0.0** (SPDX `PolyForm-Noncommercial-1.0.0`, поле
283
+ `license` в `package.json`): исходный код открыт, некоммерческое использование бесплатно, коммерческое — по
284
+ разрешению владельца (`LICENSE-COMMERCIAL.md` в пакете: что считается коммерческим использованием и как получить
285
+ лицензию — issue на https://github.com/alepri51/letopis/issues). Новая лицензия действует с 2.1.0; версии до 2.0.0
286
+ включительно выпущены под MIT и остаются под ней. `LICENSE` в корне и в пакете — полный текст PolyForm
287
+ Noncommercial 1.0.0 со строкой `Required Notice: Copyright 2026 alepri51 (https://github.com/alepri51)`; раздел
288
+ «Лицензия» в README, строки в шпаргалке и `llms.txt`. `check:docs` узнаёт лицензию по заголовку текста.
289
+
290
+ ### Документация
291
+ - `README.md`, `AGENT-CHEATSHEET.md`, `llms.txt` — под поведение 2.1: `restore({ expect })` и
292
+ `delete({ confirm, expect })`, выход по токену `auth.revoke(token)` с рецептом веб-сервиса, `auth.tenants()` с
293
+ `name` и `personal` и рецепт «арендаторы пользователя для сервиса», `withDeleted()` по шагу (таблица цепочек),
294
+ `versions()` удалённого и форма версии (`op`, `links`, `data`, `$deleted`, порядок без `sort()`), перепроверка
295
+ фильтра `update()`, `connect({ schema: 'имя' })`, отказ преобразований Zod, предупреждения, новые ключи ошибок,
296
+ `22P02` — `invalid_query` (раздел «Ошибки» и «Сырой SQL»). Удалено «id не uuid — сырая ошибка 22P02».
297
+ - Удаление и восстановление (README, раздел 7): занятость держит ключ связи; `create()` по ключу удалённого занимает
298
+ его id — перед `create()` проверка `X({ ключ }).withDeleted()`; надгробия для `expect` — из ответа `delete()` или
299
+ `withDeleted()` (строки `asOf` — живые версии); цель и каскад — в одной `db.begin()`; причина удаления — своя у
300
+ каждого удаления; таблица штатных ответов `restore()` (`not_deleted`, `restore_stale`, `target_not_found`, пустой)
301
+ с сообщением пользователю «вещь уже у другого».
302
+ - Рецепты: `syncModels` в нескольких арендаторах, текст для пользователя из `err.issues`.
303
+ - Шпаргалка (`AGENT-CHEATSHEET.md`), раздел 6 — готовый рецепт «одна открытая выдача на вещь (ключ связи по вещи),
304
+ удаление участника по превью `delete({ confirm, expect })`, возврат `restore({ expect, partial: true })` с показом
305
+ пропусков `$why`» и вариант без `partial` с разбором `err.detail.stale`; форма превью и `err.detail.preview`, форма
306
+ `expect`; новый раздел «Веб-сервис безопасно»: арендатор только из серверной сессии, одно подключение сервиса и
307
+ `svc.as(account, { tenant })` на запрос вместо `connect({ token })` на каждый вход, экранирование `& < > " '` без
308
+ обработчиков из данных, ключ сервиса — файл с правами владельца вне репозитория (то же — README, раздел 10, и
309
+ `llms.txt`).
310
+ - `versions()` по пути — только живые объекты (шпаргалка, раздел 10), история вещи, пока участник удалён, и что видно
311
+ через `withDeleted()`, когда id надгробия занят новой записью; `createTenant` — аргументы и ответ; `db.idOf` по концу
312
+ связи; `syncModels` через `svc.as()` без администраторского подключения; «Грабли» — `.nullable()`: `null`, а не
313
+ `undefined`.
314
+ - Отказы второго раунда описаны в README («Восстановить: `restore`», «Строгий режим», «Ошибки»), шпаргалке и
315
+ `llms.txt`: `write.restore_already`, `write.restore_id_taken` и `write.restore_stale` по смыслу с `err.detail.stale`
316
+ (`after`, `live`, `why`), `write.partial_without_expect`, `api.begin_arg`, `api.token_not_active` у `connect({ token })`,
317
+ граница длины строки в тексте нарушения, `api.for_update_unsupported` с помехой; атомарность
318
+ `delete({ confirm, expect })` под гонкой.
319
+
320
+ ### Ревизия движка 9
321
+ - `restore(uuid, int default null)` вместо `restore(uuid)`; `auth_tenants()` — ещё `name`, `personal`; новые
322
+ `auth_logout_token(text)`, `validate_row(text, jsonb)`, `issue_values(jsonb)`; `validate_data` — сообщения с
323
+ допустимыми значениями и лишними ключами, граница длины строки.
324
+ - `restore()`: последняя запись журнала перечитывается после ожидания блокировки; отказы `sql-time.restore_already`
325
+ (новый), `sql-time.restore_id_taken` (`detail` — `{ id, rev }`), `sql-time.restore_stale` (`detail` —
326
+ `{ id, rev, expected, after, deleted, live }`).
327
+
328
+ ## [2.0.0] — 2026-11-03
329
+
330
+ План 2.0, редакция 2 (`reports/PLAN-LETOPIS-2.0.md`, §2.1): 1.5.0 не выпускается — его работа (этап 3: права, один
331
+ процесс, ошибки и агент) входит в 2.0.0; 2.0.0 — вырезка редких фич и строгий режим (этап 4).
332
+
333
+ **Совместимости с 1.x нет.** 2.0 ставится только заново: схему letopis 1.x `up()` и `connect()` отклоняют до любых
334
+ изменений, `up({ schema, fresh: true })` пересоздаёт её, данные 1.x не переносятся. Код на API 1.x правят по разделам
335
+ «Снято» и «Строгий режим» ниже. Накат схемы проверяется в пределах 2.x: следующий выпуск 2.x проверит накат с 2.0.
336
+
337
+ ### Снято (несовместимо)
338
+ - Английский язык: `README.en.md`, `AGENT-CHEATSHEET.en.md`, `llms-full.txt` и его генератор
339
+ `scripts/gen-llms-full.mjs` (с `npm run llms:full`), корневые `README.en.md` и `CONTRIBUTING.en.md`; английские тексты
340
+ ошибок и подсказок каталога `lib/src/messages` (записи — только шаблон `ru`, подсказка — строка; тип `Lang` больше не
341
+ экспортируется); опция `lang` у `connect()`, `up()` и `createMcpServer()`, второй аргумент `describeText(model,
342
+ { lang })` и флаг `--lang` у команд CLI; английские описания инструментов MCP, их параметров и инструкции сервера.
343
+ Тексты ошибок, справки и описаний — только на русском; `err.docs` ведёт в раздел «Ошибки» `README.md`. Ошибку базы с
344
+ ключом библиотека отдаёт с текстом, который собрала база, — тем же, что видит сырой SQL; ключ и параметры — в
345
+ `err.key` и `err.params`, как прежде. Таблицы операторов в README, шпаргалке и главе «Чтение» сайта обновляет
346
+ `npm run ops:tables` (`scripts/gen-ops-tables.mjs`), сверяет `check:docs` (сверка `ops` вместо `llms-full`);
347
+ `check:docs` больше не сверяет английские версии (разделы «Tests», «Reserved names», «Errors», структуру) и `en` у
348
+ записей каталога. `lib/llms.txt` — по-русски.
349
+ - Импорт из 0.21: команда `letopis import` с её флагами (`--from`, `--from-schema`, `--tenant`, `--partition`,
350
+ `--keyless`, `--rename`, `--update`), модуль `src/import.ts`, тест `import` и фикстура 0.21
351
+ (`test/fixtures/gen-v021.mjs`, `v021.sql`), бенч против 0.21 и импорт полигона (`bench/compare.bench.mjs`,
352
+ `bench/seed-021.mjs`, `bench/import-salon.mjs`, `npm run test:import-salon`, итоги `compare` и `import-salon`, серия
353
+ `021` в `bench/core-write.mjs`) и dev-зависимость `letopis-0`. Снят `MIGRATION.md`: переход с 0.21 больше не
354
+ описывается — глава «Переход с 0.21» README (следующие главы — на номер меньше), раздел шпаргалки и ограничения импорта
355
+ сняты, глава «Производительность» — без сравнения с 0.21. Эталоны 0.21 для тестов прав и слияния (`test/oracle/`)
356
+ остались.
357
+ - Охранники API, снятого в 1.0.0, и код ошибки `removed` (записи каталога `api.removed`, `api.removed_option`,
358
+ `api.removed_many`, `api.replacement_*`, `access.replacement_*`, `write.replacement_load_trust`,
359
+ `model.cli_trust_removed`; функция `removed()` и `REMOVED_IN`): экспорты `uuidv7` и `LETOPIS_NS`; опции `connect()`
360
+ `partition`, `enforceAccount`, `enforceAcl`, `container`, `image`, `dataDir`, `redisPort`, `version`
361
+ (`REMOVED_OPTIONS`); методы цепочки `run`, `execute`, `account` и батча `execute`; свойства корня `reloadSchema`,
362
+ `accounts`, `credentials`, `resources`, `rules`, `compact`; `db.auth.sessions`, `db.acl.reload`, `watch(cb)` и
363
+ `watch(класс, cb)`; `load({ trust })` и `letopis load --trust`. Снятые имена больше не зарезервированы: `account`,
364
+ `execute`, `compact` и другие — обычные имена классов и ролей; `run` остался — это метод батча (теперь в
365
+ `BATCH_METHODS`). Неизвестную опцию называет `checkOptions` с похожими именами, неизвестный флаг CLI отклоняет разбор
366
+ аргументов. Раздел `removed` контракта API и его сверка в `check:semver` («новый охранник — мажор») убраны: снятое
367
+ имя исчезает из экспортов и списков методов, и `check:semver` требует мажор по ним.
368
+ - `letopis check` — проверки кода до запуска на TypeScript Compiler API (`src/check.ts`, флаги `--models` и `--json`,
369
+ записи каталога `model.check_*`, тест `check`) — и необязательная peer-зависимость `typescript` (для сборки она
370
+ осталась dev-зависимостью). `letopis sync --check` остался: тест `cli` (бывший `check` без правил `letopis check`).
371
+ - `db.describe()`, экспорт `describeText`, тип `ModelDescription` и команда `letopis describe` (флаг `--format`):
372
+ описание модели с примерами цепочек осталось только в инструменте MCP `letopis_model` — внутренний модуль
373
+ `src/describe.ts`, его нет в `exports`; поле `methods` описания и подписи текста `time.describe_*` сняты. Тест
374
+ `describe-examples` стал `mcp-model`: цепочки `examples` из ответа `letopis_model`, а также метаданные классов с
375
+ наследованием и условия `anyOf` и `oneOf` — проверки описания из `telemetry`.
376
+ - Сброс `db.reset({ level, confirm, tenant })`: опция `up({ allowReset })` (и флаг `--allow-reset` у `db/apply.mjs`),
377
+ функции базы `reset` и `reset_mark`, коды ошибок `reset_disabled`
378
+ (LT019) и `confirm_mismatch` (LT020), эндпоинт прав `letopis.reset`, служебная запись журнала вида `reset` (класс
379
+ `#reset`) и служебные сигналы `*` и `session:*` — их слал только сброс (`session:<тег>` погашенной сессии остался;
380
+ `*` и `session:*` от посторонней роли — теперь просто нагрузка не по режиму); типы `ResetOptions` и `ResetResult`.
381
+ `maintain({ now })` пускала только установка с `allowReset`; сама опция снята вместе с прореживанием (ниже). Первая
382
+ заливка больше не считает «пустой» схему с
383
+ записями сброса. Тесты: `reset` снят, сброс в `cache`, `signals`, `initial-load` (схема заново — `install(…, { fresh:
384
+ true })`), `hash`, `verify`, `roles`, `security-final` и `scenario-salon` — тоже.
385
+ - Совместимость с 1.x: 2.0 ставится только заново, ревизия движка — 8. `up()` на схеме letopis 1.x — ревизии 7 и
386
+ ниже или без штампа ревизии в комментарии схемы — отказывает `invalid_data` (ключ каталога `boot.schema_1x`) до любых
387
+ изменений, в том числе с `upgrade: true`; подсказка — `up({ schema, fresh: true })`: схема пересоздаётся, данные 1.x
388
+ не переносятся. `connect()` к такой схеме теперь тоже отказывает — до входа по ключу (было предупреждение «обновите:
389
+ up({ upgrade: true })»). Сняты миграции 0006 (колонка `log.changed`) и 0007 (внешний ключ `secret.account`) — в
390
+ свежей установке их ставит `sql/10-core.sql`; каталог `sql/migrate/` остался для миграций 2.x, их накатывает
391
+ `up({ upgrade: true })` в пределах 2.x, правило имени — README каталога. Сняты тест `upgrade` с dev-зависимостями —
392
+ пакетами прежних выпусков 1.0.1, 1.1.0, 1.4.1 и 1.4.2 (`npm uninstall`) — и бенч против 1.0.1 с его итогами в
393
+ `bench/results/`. Из файлов ядра ушло то, что служило только накату со схем 1.x: `drop … if exists` прежних сигнатур
394
+ функций (`family_lock`, `links_shape`, `targets_bad`, `check_links`, `signal`, `move_row`, `verify`, `row_visible`,
395
+ `acl_ok`, `version_changes`, `version_leaves`, `issue_session`, `auth_issue_otp`, `login_otp`, `derived_clear`,
396
+ `agg_classes`, `projector_behind`, `maintain`, `watch_head`, `watch_read`), функций сброса `reset` и `reset_mark`,
397
+ индекса `log_class_at` и строкового триггера `entity_ai` (теперь `create or replace trigger`), пометка удалёнными
398
+ строк `LOCK` других аккаунтов при накате (их заводили креды 1.1). В библиотеке — запасные вызовы для схем 1.x: нет
399
+ `signal_mode()` — режим `class`, у `maintain()` нет третьего аргумента — прежний вызов. Предупреждение `connect()`
400
+ «функции схемы поставила версия до 1.4.0» (штамп без версии движка) стало «поставила неизвестная версия letopis»
401
+ (`boot.stamp_unknown` вместо `boot.stamp_before_140`). Правило «каждый выпуск проверяет накат с предыдущего»
402
+ действует с 2.0: следующий выпуск 2.x проверит накат с 2.0. Тест `up`: «план 2.0, 7.10 №5» (схема 1.x — отказ `up()`
403
+ и `connect()` с подсказкой, `fresh` проходит) и механизм наката миграций 2.x; тест 7.10 №3 (хаб с зарезервированным
404
+ именем переживает `up()` с предупреждением) перенесён из `upgrade` в `up` — хаб заводит роль владельца мимо проверки
405
+ базы, а не пакет 1.4.2; `engine-races` №3в — без пометки строк `LOCK` накатом.
406
+ - Агрегаты в базе и проекторы (план 1.1, §2.4). Агрегаты: опция класса `aggregate` (тип `AggregateDef` в
407
+ `letopis/model`, тип `AggregateInfo` и поле `aggregate` у `ClassInfo`), `db.rebuildAggregates()`, триггеры
408
+ `entity_total_ai`, `entity_total_au`, `entity_total_ad` и функции `agg_*`. Проекторы: `db.projector()` с типами
409
+ `Projector`, `ProjectorOptions`, `ProjectorChange`, `ProjectorOut`, `ProjectorRow`, `ProjectorReport`,
410
+ `ProjectorStatus`, `ProjectorFailure`, опция класса `derivedBy` (производные классы; поле `derivedBy` у `ClassInfo`),
411
+ чекпоинты — строки `secret` вида `PROJECTOR`, функции `projector_state`, `projector_advance`, `derived_*`,
412
+ `projector_unread` и параметр `p_leaving` у `watch_read`. Сняты файлы `sql/88-derived.sql`, `src/project.ts` и
413
+ раздел каталога сообщений `derived`. Коды ошибок `derived_class` (LT021), `aggregate_field` (LT022),
414
+ `projector_pending` (LT023), `rebuild_required` и `projector_busy`. Метасхема `Class` не принимает `derivedBy` и
415
+ `aggregate` (`invalid_data`); описание модели в `letopis_model` — без них. Вид креда `PROJECTOR` больше не служебный.
416
+ `targets_bad` больше не отличает цель производного класса, `class_admit` не берёт блокировку семейств классов-целей
417
+ концов (её брали только ради пометки `derivedBy`), `syncModels` упорядочивает модели только по родителям. Имена
418
+ `projector` и `rebuildAggregates` больше не зарезервированы. Тексты отказов: «класс меняет предков или алиас» (было
419
+ «предков, алиас или derivedBy»), пересоздание класса с историей строк — «с прежними parent, alias и history». Второй
420
+ пул `connect({ admin })` — для `load`, `maintain` и `syncModels`. Сняты тесты `projector` и `aggregate`,
421
+ атаки №33, №34, №38, №39, №44, №50, №52, №53, №55 и №57 (номера остальных прежние), случаи проектора и
422
+ `rebuildAggregates` в `disconnect`, `messages`, `one-process` и `watch-contract` (7.4 №5, №6), агрегатов — в
423
+ `initial-load` (7.4 №10) и `sync`; бенч `bench/derived.bench.mjs` с итогами `derived*`. README — без глав «Агрегаты в
424
+ базе», «Рецепт: баланс агрегатом», «Проекторы», «Рецепт: дневная выручка», «Проектор» и ограничений проекторов и
425
+ агрегатов, глава «Производительность» — без замеров производных данных; шпаргалка — без разделов о них и граблей
426
+ №19–20 (раздел «Агенты и MCP» — теперь 11). Агрегаты чтения (`sum`, `avg`, `min`, `max`, `countBy`) остались. Долгий
427
+ прогон `bench/soak.mjs` переписан без агрегата и проектора (ниже, «Обвязка и проверка»).
428
+ - `diff()` и `blame()` (план 1.1, §2.1): терминалы цепочки, типы `DiffResult`, `DiffChange`, `DiffOptions`,
429
+ `BlameResult`, `BlameField`, `BlameOptions`, файл `sql/75-history.sql` (функции `jsonb_changes`, `row_changes`,
430
+ `change_paths`, `row_leaves`, `version_paths`, `history_segments`, `diff_versions`, `blame`) и колонка журнала
431
+ `log.changed`; записи каталога `api.version_or_moment`, `api.version_number`, `api.moment`. Журнал выдан приложению
432
+ всеми колонками: `select * from log` под RLS работает. Инструмент MCP `letopis_history` отдаёт только версии:
433
+ `kind` стал необязательным и принимает только `versions`, параметров `from`, `to`, `at`, `limit` нет (записи
434
+ `mcp.history_param`, `mcp.blame_follow`); причину записи агента показывают версии. Тест `history` — только причина
435
+ изменения (7.2 №5 и №8), `mcp` №9 — причина в версиях; бенч
436
+ `bench/history.bench.mjs` с итогом снят. Имена `diff` и `blame` больше не зарезервированы.
437
+ - Политика хранения истории и прореживание журнала (§2.11): у класса `history` — только `true` или `false` (объект —
438
+ `invalid_data` метасхемы `Class`), тип `HistoryPolicy` (`letopis`, `letopis/model`), опция `up({ history })` и
439
+ подстановка `@HISTORY_DEFAULT@`, функции базы `history_default`, `history_policy`, `history_iv`, `history_horizon`,
440
+ `history_keep`, `thin_batch`, метки `thin`, `thin_now` и `default_policy` строки ключа, тексты отказов про ярусы
441
+ (`sql-core.history_*`, `sql-core.system_history_policy`). `maintain()` журнал больше не трогает: одной транзакцией
442
+ удаляет сессии, коды и строки-замки удалённых классов и проверяет очередь сигналов; опции `maintain({ now, batches,
443
+ limit })`, тип `MaintainOptions`, поля отчёта `thinned`, `batches` и `more`, флаг `--batches` у `letopis maintain`,
444
+ параметры `maintain()` базы и запись `sql-time.maintain_now` сняты. Обрезка истории: `db.trimHistory()`, функция базы
445
+ `trim_history` и служебная запись журнала вида `trim` (записи `sql-time.trim_*`). Код ошибки `cursor_expired` (LT016):
446
+ курсор подписки больше не устаревает — версии журнала стирает только удаление аккаунта, а оно `cursor_expired` не
447
+ давало; время в курсоре не нужно, курсор — `tx:seq[~снимок]`; параметр `p_at` у `watch_read`
448
+ снят. Отказ смены `history` без права на все строки — «класс … включает или выключает журнал (history)». Настройки
449
+ `autovacuum_*_scale_factor = 0.02` у журнала сняты вместе с прореживанием. Сняты тест `retention`, тесты `maintain`
450
+ 7.3 №4 (очистка в первой пачке), `time` 6.4 (обрезка), обрезка в `load` (7.6.1 №9), `security-final` и акте 14
451
+ `scenario-salon`; `engine-class` №17 и атака `engine-attacks` №43 — про `history: false`. Имя `trimHistory` больше
452
+ не зарезервировано.
453
+ - Хэши версий и `verify()`: колонки `hash` (генерируемая) и `prev_hash` у `entity` и `log`, функции `snapshot` и
454
+ `version_hash`, `verify()` базы (право `letopis.verify`), `db.verify()` с типами `VerifyResult` и `VerifyIssue`,
455
+ команда `letopis verify` (`--data`), опция `load({ verify })` и флаг `letopis load --verify` (записи каталога
456
+ `write.load_db_invalid`, `write.load_db_target`), записи `sql-time.verify_*`. Режим истории загрузчика пишет версии
457
+ вставками по 5000 строк в прежнем порядке (по номеру версии), без волн с хэшами. Сняты тесты `hash` и `verify`, атака
458
+ `engine-attacks` №19 (правку в обход триггеров находил `verify()`; номер заново не занимается); вместо `verify()` в
459
+ тестах гонок и сценариях журнал сверяет помощник обвязки `journalIssues` (номера версий подряд, строка — последняя
460
+ версия). Бенч `bench/gencols.bench.mjs` мерит только `link_ids`. Имя `verify` больше не зарезервировано.
461
+ - Смена ключа и класса, затирание полей и стирание истории объекта: глаголы цепочки `rekey()`, `reclass()`,
462
+ `anonymize()` и `purge()` (с полями строки ответа `moved`, `$versions` и `$purged`), `db.rekeyClass()`,
463
+ `versions({ follow })` и параметр `follow` у `letopis_history`; функции базы `rekey`, `rekey_class`, `reclass`,
464
+ `move_row`, `links_replace`, `purge` и `watch_leaving`, колонка `moved` у `entity` и `log` с индексом `log_moved`,
465
+ служебная запись журнала вида `purge`. Класс, ключ и id строки неизменны: смена класса правкой — сырым
466
+ `UPDATE … SET class`, в том числе у строк `Class` и строк прав, — `immutable_key` («класс строки … неизменен», запись
467
+ `sql-write.class_immutable`); объект другого класса или с другим ключом — новый объект. Коды ошибок `use_reclass`
468
+ (LT011) и `reclass_denied` (LT012) сняты, номера заново не заняты; `isolation_level` — только у удаления,
469
+ `not_deleted` — только у `restore`. Записи каталога `write.anonymize_*`, `write.rekey_*`, `write.not_deleted_purge`,
470
+ `sql-write.class_row_kind`, `sql-write.system_row_reclass`, `sql-write.reclass_*`, `sql-write.use_reclass`,
471
+ `sql-write.rekey_needs_all`, `sql-write.refs_*`, `sql-write.target_not_found`, `sql-write.row_write_denied`,
472
+ `sql-write.class_write_denied`, `sql-time.purge_*`, `sql-time.rekey_*`, `sql-time.no_delete_class`,
473
+ `sql-time.no_write_class` и `sql-time.refs_denied*` сняты. Подписка читает журнал одной функцией `watch_read`:
474
+ событий ухода строки из классов подписки сменой класса больше нет. Историю строк не стереть: класс, у которого она
475
+ есть, не удаляется (класс, который придётся удалять, заводят с `history: false`), алиас предка удалённых потомков с
476
+ историей строк не меняется; версии журнала стирает только удаление аккаунта (`db.auth.purgeAccount`). Тексты отказов
477
+ удаления и пересоздания класса — без совета стереть историю. Сняты тест `rekey`, тесты `time` 6.1 (через смену id) и
478
+ 6.3, `chain-write` 4.8 и обезличивание, `read-contract` 7.1 №1 и №2, `watch-contract` 7.4 №4, `signals` 7.3 №16,
479
+ гонка `engine-races` №5, атака `engine-attacks` №42 (номер заново не занимается), события ухода в `watch`, «находка 1»
480
+ о `rekeyClass` в `security-final`, акт 16 `scenario-salon` и случай `load` 7.6.1 №9 после стирания истории; без смены
481
+ ключа и класса — `write-contract` 7.2 №2, `cache` №14, акт 13 `scenario-salon`, `chain-api`. Атака №5 и новый тест
482
+ `engine-write` — смена класса правкой даёт `immutable_key` у строки без ключа, с ключом и строки `Class`;
483
+ `write-contract` 7.2 №4 переводит строку в другой класс от роли владельца. README — без глав «Сменить ключ или класс»
484
+ (вместо неё — «Ключ и класс неизменны»), «Обезличить» и «Стереть историю»; шпаргалка — без смены ключа,
485
+ обезличивания, стирания истории и `follow`. Имена `rekey`, `reclass`, `anonymize`, `purge` и `rekeyClass` больше не
486
+ зарезервированы.
487
+
488
+ ### Строгий режим (этап 4; несовместимо)
489
+ - Строгие типы цепочек (7.10 №1, №6). `connect({ models })` и `connect<Models>()` с объявлениями `letopis types` дают
490
+ `TypedDb` без индексных сигнатур: шаг от корня — только класс или алиас моделей и системных классов (встроенный набор
491
+ `SystemModels`: `Class`, `Account`, `member`, `Resource`, `rule`; своя модель с тем же именем — вместо встроенной);
492
+ переходы выводятся из концов, как в `resolveHop`; слоты — только роли записываемого класса: `set(id | строка |
493
+ цепочка)`, `add` и `remove` — у множественного конца, `unset` — у необязательного; поля фильтров, `or()` и агрегатов —
494
+ поля класса (`sum` и `avg` — числовые). `letopis types` выдаёт ещё `<Класс>Model` — имя, алиас, концы с классами
495
+ целей и предки — и список `Models`: строгие типы работают и без моделей Zod. Новый экспорт `loose(db)` — то же
496
+ подключение без строгих типов для имён, известных только во время выполнения; типы `SystemModels`, `OrOp`, `ChainRef`.
497
+ `entity()` и значение слота принимают любую цепочку (`ChainRef`), `createMcpServer` и `syncModels` — подключение со
498
+ строгими типами. Модели без литеральных имён (`AnyModel[]`) строгих типов не дают — подключение `Db`, как прежде.
499
+ `db.as()`, `db.begin()`, `db.batch()` и `entity()` отдают цепочки без строгих типов.
500
+ - `update()` и `delete({ confirm: true })`, а ни у одного шага нет фильтра, id, `tags()`, `owner()` или `limit()` (и
501
+ раньше в цепочке нет `create()` или `upsert()`, а у `update` — `data.id`), — `invalid_query` до записи (запись
502
+ каталога `write.no_filter`): в цепочке, в батче (`run()` не пишет ни одного плана) и в `db.begin()` (транзакция
503
+ цела). Все строки шага — опция `{ all: true }`; превью `delete()` — без условий. Раньше такая правка шла по всем
504
+ строкам шага (7.10 №2). `update(патч, { rev, all: true })` с несколькими целями — по-прежнему `rev_ambiguous`.
505
+ - Неизвестная опция `connect()`, `up()`, `db.load()`, `db.watch()` и `createMcpServer()` — `invalid_data` с похожими
506
+ именами (`api.unknown_option`, `err.params.similar`) до подключения и прочих проверок; в работе 1.5 (этап 3) было
507
+ предупреждение (7.10 №4). `db.load()` сверяет опции и до пула администратора `connect({ admin })`.
508
+ - Неизвестное поле фильтра (и внутри `or()`), `sort('data.…')` или агрегата — `invalid_query` с похожими именами
509
+ (`query.unknown_field`, `err.params.similar`; план 2.0, этап 3, п. 3): поле не описано ни в одном классе шага —
510
+ классе, его потомках (если шаг не `exact()`), у роли — классах её целей; в фильтре годится и роль конца. В работе 1.5
511
+ было предупреждение `console.warn` с хвостом «(в 2.0 — ошибка invalid_query)», один раз на имя за процесс; хвост и
512
+ опция `future` у `lwarn` сняты. Сверяется только первый ключ пути (путь внутрь поля-объекта — нет), как и прежде.
513
+ У строк системного класса `Class` поля — свойства метасхемы описания класса (`kind`, `alias`, `parent`, `fields` и
514
+ другие), а не описание самого `Class` с одним `name`: прежнее предупреждение на `Class({ kind })` было ложным, отказа
515
+ нет. План записи проверяет фильтры, `sort()` целей и агрегат до первого оператора (`precheckStep` в `precheckPlan`):
516
+ опечатка в фильтре цели не ломает `db.begin()`; чтение — до запроса.
517
+ - `after()` перед `count()` и агрегатами чтения `sum`, `avg`, `min`, `max`, `countBy` — `invalid_query`
518
+ (`api.after_not_paged`) до запроса; раньше курсор там молча не учитывался (7.10 №7).
519
+ - Хаб с зарезервированным именем (`RESERVED_CLASS_NAMES`) отклоняют `hub()` (`model.hub_name_reserved`), `syncModels`
520
+ — модель, собранную мимо `hub()` (`model.sync_hub_name_reserved`), — и база (`class_admit`,
521
+ `sql-write.hub_name_reserved`); связь с таким именем база отклоняла и раньше. Хаб, заведённый мимо проверки базы,
522
+ `up()` оставляет и называет в предупреждении (`boot.reserved_hubs`), а правку его описания база отклоняет (7.10 №3).
523
+ - Документы: README — раздел «Строгий режим» (глава 4, вместо «Типы цепочек»), правила `update` и удаления, курсор,
524
+ «Зарезервированные имена», «Ошибки» и «Ограничения»; шпаргалка и `llms.txt` — так же. Правка с `{ rev }` больше не
525
+ называется «строгим режимом». Неизвестное поле фильтра, `sort()` и агрегата — отказ (выше), README, шпаргалка и
526
+ `llms.txt` — так же.
527
+
528
+ ### Сервисы-примеры и сайт (на урезанном API)
529
+ - Сервисы `services/` переведены на 2.0 и снова пройдены в Chrome по `CHECK.md` (3 ноября 2026 года). wallet: баланс —
530
+ поле счёта с `min(0)`, операция — один план `Wallet(счёт).Movement().create(…).Wallet().update({ balance: inc(amount) })`
531
+ с причиной у обоих глаголов, перевод — две операции в `db.begin()` под замками в порядке id, нехватка средств —
532
+ `invalid_data` по `minimum`; выписка по месяцам — запрос `group by` в транзакции сессии вместо проектора; агрегат,
533
+ производный класс `Statement`, перестройка и `verify` сняты. documents: подписание без `reclass` — одна транзакция
534
+ `db.begin({ reason })` создаёт `Contract` с данными черновика и удаляет `Draft` (договор — новый объект: класс входит в
535
+ v5-id), лента версий — версии обоих id; надгробие подписанного черновика не попадает в корзину (`restore` — 409),
536
+ новый черновик с номером договора — 422; смена номера, `diff`/`blame`, ярусы, обрезка, обезличивание, стирание,
537
+ `verify` и `follow` сняты, остались версии с причинами, `asOf`, `withDeleted`, превью удаления, `restore`, первая
538
+ заливка архива и `maintain()`. helpdesk: «Удалить из компании» снимает только членство (без `anonymize`) — строки
539
+ человека в компании база передаёт компании. salon: перенос брони — одна транзакция под замками мастеров: бронь
540
+ перечитывается `forUpdate()`, новая бронь на новом слоте с теми же концами, прежняя удаляется (время начала в ключе,
541
+ ключ неизменен — у новой брони новый id); надгробие переноса узнаётся по причине версии, история — `versions()` одного
542
+ id без `follow`. Сценарии: wallet 21 из 21, documents 24 из 24, helpdesk 74 из 74, salon 27 из 27.
543
+ - Сайт `guide/`: 51 исполняемый пример, снимки перезаписаны. Сняты примеры `derived/01–04`,
544
+ `history/02-follow-rekey-reclass`, `history/03-retention-maintain-trim`, `ops/01-verify-corrupt`, `ops/02-reset` и
545
+ глава «Агрегаты и проекторы» (главы перенумерованы: 12 «Массовая загрузка» … 16 «Все нюансы»); переписаны
546
+ `history/01-versions-asof`, `delete/02-restore`, `write/04-key-class-immutable`, `errors/03-strict-mcp-errors`
547
+ (строгий режим), `ops/02-sql-cache-signals`, `ops/03-cli`, `watch/02-quiet-cursor` (курсор не устаревает); новый
548
+ `ops/01-maintain` — отчёт `{ skipped, sessions, codes, queue }`. Раздел «Строгий режим» в главе моделей, нюансы
549
+ строгого режима с отметкой «ловит код», глоссарий без английской колонки и терминов снятого, ER-схемы и справочник —
550
+ из `db.registry` вместо `db.describe()`; покрытие API — 323 из 323.
551
+
552
+ ### Обвязка и проверка
553
+ - Долгий прогон `bench/soak.mjs` (план 2.0, §7.7) — без агрегата, производного класса и проектора, ярусов с засевкой
554
+ старой истории, `trimHistory`, `maintain({ batches })` и `verify`. Нагрузка прежняя по составу: писатели — заказ
555
+ горячего магазина и приращение его суммы `inc(amount)` одним планом, `inc` горячих счётчиков, правка последнего
556
+ заказа; читатели (цепочки с `limit`, ключ, `asOf`); подписка надолго и «открыл-закрыл»; `maintain()` раз в минуту;
557
+ вход по паролю и по коду с выходом; `db.as()` по 40 арендаторам; заведение и удаление классов. В конце вместо
558
+ `verify({ data: true })` — сверка журнала запросом (номера версий подряд, строка — последняя версия; тот же запрос,
559
+ что `journalIssues` обвязки тестов). В итоге `soak.md` — применимые критерии §7.7 (1–4, 6 и сверка журнала вместо 5);
560
+ снятые (5 — `verify`, 7 — отставание проектора, прореживание под записью) — строкой «снят в 2.0.0».
561
+ - Глава «Производительность» README — на сериях `core-*` (точка А) и `chain-read`, без чисел полигона salon; числа в
562
+ «Когда нужен загрузчик» и «Ограничениях» — из `core-write`.
563
+ - Текст отказа `maintain()` внутри `db.begin()` (`time.maintain_in_tx`): «maintain() идёт своей транзакцией, не внутри
564
+ db.begin()» — было «пачками в своих транзакциях»: прореживания пачками больше нет.
565
+ - Итоговая сверка документов с кодом. README: опции `up()` (`login`, `migrationsDir`, поле результата `login`) и
566
+ `connect()` (`admin`), `watch({ batch })`, поле `id` у `acl.list()`, границы строгого режима (сужает шаг глагола и шаги
567
+ до него, пустой `tags([])` не сужает, в батче отказ — при `run()`, связь с зарезервированным именем отклоняет база,
568
+ строгие типы — только у подключения с моделями); без «проверки» у администраторского подключения, «до 1.5.0»,
569
+ «ревизии 7» и перехода масок `Resource` с 1.x. Шпаргалка: строгий режим строкой в «Модели за минуту», опции `up()`,
570
+ `connect()` и `load()` полностью, отчёт `maintain()`, класс с историей строк не удаляется, без события ухода из классов
571
+ подписки, «Грабли 2.0». `llms.txt`, `lib/sql/README.md` (2.0, `@OWNER_NAME@`), `CONTRIBUTING.md`; пометки «снято в
572
+ 2.0.0» в `BACKLOG.md` и `COMPARISON.md`.
573
+ - `docs/checkpoint-guide.md` — пометка: исторический снимок, часть примеров снята в 2.0.0, числа устарели. Итог
574
+ `npm run prerelease` — `docs/checkpoint-2.0.0.md`.
575
+
576
+ ### Этап 3 (бывший 1.5.0)
577
+
578
+ ### Добавлено
579
+ - Обёртка прав `db.acl` (7.9 №1): `grant({ role | everyone: true, read, write, delete, api, weight })`, `deny(…)`,
580
+ `revoke(…)`, `roles(аккаунт, роли)` и `list()` — те же строки `Resource`, `rule` и `member` текущего арендатора с
581
+ детерминированными alias (`role:staff:ACCOUNT`, `everyone:ACCOUNT`, `class:Product:READ`, `api:reports.*:API`),
582
+ ключ правила — пара группа и объект, вес по умолчанию 50 — сильнее «можно всё» от `createTenant`. Повтор ничего не
583
+ меняет; права на запись правил, RLS и триггеры проверяет база; внутри `db.begin()` — в её транзакции. `everyone:
584
+ true` вместо `role` — правило для всех аккаунтов арендатора; `revoke({ everyone: true, read: '*', write: '*',
585
+ delete: '*' })` снимает «можно всё» от `createTenant` — права только по ролям (агентский прогон: снять его обёрткой
586
+ было нельзя, только удалением строк `rule` по id). `list()` отдаёт `{ role, everyone, group, op, target, condition,
587
+ permission, weight, enabled }` (`enabled: false` — правило выключено, база его не применяет) и на администраторском
588
+ подключении — только правила арендатора сессии (роль без RLS видит строки всех арендаторов). Типы `AclRule`,
589
+ `AclRuleSpec`, `AclTarget`.
590
+ - Один процесс для малых приложений (7.9 №3, №4): `up({ login: { user, password } })` заводит роль приложения или
591
+ обновляет её пароль и выдаёт `letopis_app` — имя и пароль квотирует база, привилегированную роль `up()` не трогает,
592
+ строка подключения роли — `r.login.dsn` (тип `LoginOptions`); `connect({ dsn, admin, token })` открывает второй пул
593
+ (не больше двух соединений) при первой надобности — для `load`, `maintain` и `syncModels` с той же сессией,
594
+ цепочки и `db.sql` остаются в пуле приложения под RLS.
595
+ - Все тексты ошибок библиотеки и базы — ключи каталога `lib/src/messages`: около 500 мест; `check:docs` следит, чтобы
596
+ литеральных текстов не осталось. Отказ инструмента MCP отдаёт `hint` и `similar`. Отказы `up()` с подходящим кодом —
597
+ `LetopisError` (`invalid_data`, `admin_required`), а не обычный `Error`.
598
+ - Тексты `up()` — ключи каталога: ошибки и предупреждения `up()`, в том числе заведения арендатора `tenant`, и ход
599
+ установки в консоли. Отказы без подходящего кода (версия сервера, кодировка базы, срок ожидания, ревизия схемы новее
600
+ или старше пакета, нет пакета PGlite) остаются обычным `Error` с префиксом `letopis.up:`, текст — тоже из каталога.
601
+ На ключи каталога переведены и тексты для пользователя и агента: предупреждение `connect()` о штампе движка, хвост
602
+ «откатите транзакцию и повторите весь блок `db.begin()`» у 40P01 и 40001, отчёт `letopis sync` и пометка
603
+ `letopis types`, пояснения к данным в ответах MCP, первая строка отказа инструмента, подсказка `confirm` превью, окно
604
+ и поле формы подтверждения удаления.
605
+ - `letopis types --system` — объявления и для классов арендатора System: системных и классов сидов пакета (например,
606
+ `booking`); без флага, как прежде, — только классы арендатора сессии.
607
+ - Тест исправления 7.5 №3, у которого его не было: установка без ADMIN OPTION на готовую роль владельца —
608
+ `admin_required` (код 42501 подтверждён на PostgreSQL 18.6); без исправления он падает.
609
+ - Каталог сообщений (каркас): `lib/src/messages` — ключ `<раздел>.<имя>` → код ошибки, шаблон с параметрами `{имя}` и
610
+ подсказка; у каждого кода — заголовок и подсказка (`codes.ts`), из них же строится страница ошибок сайта. Русские
611
+ тексты прежние. У `LetopisError` новые поля: `hint` — что делать, `docs` — ссылка на строку
612
+ кода в разделе «Ошибки» README на GitHub по тегу версии, `key` и `params` — ключ и параметры. База кладёт ключ и
613
+ параметры в `DETAIL`: перегрузки `lt_fail(ключ, параметры, detail)` и `lt_raise(код, ключ, параметры, detail)`,
614
+ `lt_text()` и `message_catalog()` — разделы `sql-*` каталога подставляет обычный `up()`, без новой ревизии; сырой SQL
615
+ видит прежний русский текст. На ключи переведены образцы: неизвестный класс или роль, `ValidationError`,
616
+ `connect({ schema })`, поле `sort()`, `auth.switch()` у `db.as()`, а в базе — нет членства в арендаторе, данные не
617
+ проходят описание класса, последний владелец арендатора; затем — все остальные места (пункт выше и пункт об
618
+ остатке ниже). `check:docs` сверяет полноту каталога (`messages`) и литеральные тексты ошибок (`literals`; список
619
+ `PENDING_LITERALS` пуст). Код `deleted` загрузчика вошёл в контракт API.
620
+ - Каталог сообщений, остаток: ключи каталога и у нарушений данных из базы — запись (`validate_data`, `inc`),
621
+ ужесточение класса (`err.detail.rows[].problem`), загрузчик (строки классов с `pattern`). Нарушение базы несёт
622
+ ключ каталога и параметры (`key`, `params`): библиотека их убирает, форма прежняя — `{ path, keyword, message }`
623
+ и `problem`; сырой SQL видит прежний русский текст и рядом ключ. Повторяемый конфликт с ключом (`40001`, `40P01`)
624
+ остаётся ошибкой базы с тем же SQLSTATE — на нём держатся повторы. `check:docs` (`literals`) ловит в `lib/sql`
625
+ ещё `raise` с литеральным текстом и литерал в `problem`. Источник: ревью.
626
+ - Новый код `internal`: строка-замок семейства не создана или не найдена (`lock_row`, `family_lock`). Раньше — голый
627
+ `XX000` с русским текстом без префикса, теперь `letopis: internal: строка-замок … не создана` с ключом в `DETAIL`, а
628
+ в библиотеке — `LetopisError` с кодом `internal` и подсказкой «сообщите о ней». Голые
629
+ `XX000` самого PostgreSQL — как прежде. Источник: ревью.
630
+ - Похожие имена (7.9 №6): общий `similar()` (`errors.ts`, его берёт и `letopis/mcp`). Неизвестный класс или роль в
631
+ цепочке — `invalid_class`, к тексту дописаны похожие имена реестра («— похожие: Shop»), они же в
632
+ `err.params.similar`. Неизвестное поле фильтра, `sort()` или агрегата — предупреждение `console.warn` с похожими
633
+ именами, один раз на имя; неизвестная опция `connect()`, `up()`, `load()`, `watch()` и `createMcpServer()` — тоже было
634
+ предупреждение; в 2.0.0 и то и другое стало отказом (раздел «Строгий режим» выше).
635
+ - Примеры цепочек в описании модели (7.9 №9): у каждого класса в ответе инструмента MCP `letopis_model` — `examples`,
636
+ рабочие цепочки из реестра: `create` (поля ключа и обязательные поля с образцами значений, обязательные концы —
637
+ слотами; нет у абстрактного и системного класса), `find` (по ключу или обязательному полю) и
638
+ `neighbors` (по концам класса и к классам, чьи концы ведут на него); `'<id Класс>'` — место для id. Образец значения —
639
+ `examples` из JSON Schema поля, `const`, `default`, `enum`, иначе по типу и формату. Модель больше `maxBytes`
640
+ приходит сначала без `examples`, затем краткой. Тест `mcp-model` исполняет каждую цепочку на демо-схеме.
641
+ - Один словарь фильтров (7.9 №8): реестр операторов `OPERATORS` в `src/ops.ts` — имя фабрики в TypeScript, ключ
642
+ JSON-фильтра MCP, вид его значения и смысл. Из реестра строятся таблица `OPS` и список операторов в описании
643
+ `letopis_query`, таблица «TypeScript — MCP — смысл» в README (вместо прежней из семи строк) и в шпаргалке, в
644
+ справочнике сайта и в главе «Чтение» сайта (без колонки MCP, вместо ручной таблицы из семи строк); в абзаце о MCP
645
+ README и в описании `letopis_query` справочника вместо ручного списка ключей — ссылка на неё. Таблицы с реестром
646
+ сверяет `check:docs`. Тест `llms`: реестр совпадает с фабриками пакета, ответами `letopis_query` и таблицами README,
647
+ шпаргалки и главы «Чтение» сайта.
648
+ - `letopis sync --check` (план 2.0, этап 3, п. 8; 7.9 №10) — сухой прогон с кодом 1, если он нашёл изменения: шаг CI
649
+ перед `--apply`; вместе с `--apply` — справка и код 2. Тест `cli`.
650
+
651
+ ### Исправлено
652
+ - Кэш результатов копил счётчик на каждую погашенную сессию: сигнал `session:<тег>` заводил запись в карте счётчиков
653
+ кэша каждого подключения, даже если сессия через это подключение не читала. Процесс, который впускает и выпускает
654
+ пользователей (вход по паролю и по коду, `revokeAll`), рос на 0,7–0,9 МБ в час на подключение до предела в 100 000
655
+ записей. Теперь счётчик сессии заводит снимок запроса через кэш, а сигнал меняет только заведённые; ответы погашенной
656
+ сессии сбрасываются, как прежде (план 2.0, 7.7). Источник: soak.
657
+ - Валидатор на TypeScript (`compileValidator`, проверка до записи в цепочках и загрузчике) исполнял шаблон `pattern`
658
+ из описания класса — его задаёт арендатор — движком V8 на данных: шаблон с катастрофическим перебором (`^(a+)+$` на
659
+ строке из 40 «a» и «!») вешал процесс приложения на часы. Теперь `pattern`, а также `anyOf` и `oneOf` с ним в ветке,
660
+ валидатор только разбирает (неверный шаблон — по-прежнему `invalid_class`), а проверяет база (`like_regex`, движок
661
+ PostgreSQL без такого перебора): при записи цепочкой — триггер, нарушение — тот же `ValidationError` (`invalid_data`);
662
+ в `db.begin()` оно, как другие ошибки базы, обрывает транзакцию. Загрузчик пишет мимо триггеров, поэтому строки
663
+ классов с `pattern` он сверяет запросом к базе на пачку до записи — нарушение в отчёте с номером строки, как прежде;
664
+ у классов без `pattern` запросов на пачку по-прежнему нет. Источник: ревью.
665
+ - Администраторское подключение (`allowBypassRls: true`) с сессией пользователя без прав не могло записать правило:
666
+ строки `Resource` оно заводило (RLS на нём не действует), а `rule` со слотами `.Group.set()` и `.Object.set()` на
667
+ них и `db.acl.grant` отвечали `target_not_found`. База проверяла, видит ли цели концов актор сессии, хотя роль
668
+ подключения видит любую строку. Так было у арендатора без «можно всё» (`grantAll: false`) и после снятия этого
669
+ правила. Теперь видимость целей по правам актора проверяется только у роли сеанса под RLS; суперпользователь, роль с
670
+ `BYPASSRLS` и член роли-владельца её пропускают. Существование цели,
671
+ её арендатор и класс проверяются, как прежде. Функцию обновляет обычный `up()`, новой ревизии нет. Рецепт — раздел
672
+ README «Права только по ролям» (агентский прогон плана 2.0, этап 3, п. 9). Источник: внедрение.
673
+ - Валидатор на TypeScript проверял `format: 'email'` шаблоном, который на подобранной строке (много точек в домене
674
+ и `@` в конце) перебирает квадратично: строка в мегабайт занимала процесс на минуты. Теперь адрес длиннее 254
675
+ символов (предел адреса по RFC 5321) — нарушение `format` без шаблона. Проверка в базе прежняя: длину она не
676
+ ограничивает, а движок PostgreSQL такого перебора не делает. Источник: ревью.
677
+ - Тесты исправлений 1.4.2, у которых их не было: 7.4 №7 — новый файл `disconnect` (обрыв TCP до и после COMMIT и на
678
+ открытии транзакции, `pg_terminate_backend` процесса, ждущего блокировку, — в транзакции загрузчика, первой заливки и
679
+ при открытии `db.begin()`: промис отклонён ошибкой связи, необработанных ошибок в процессе нет, следующая операция
680
+ пула проходит); 7.5 №3 — `impersonation` (`auth.switch` при имперсонации сырым SQL — `LetopisError` `acl_denied`,
681
+ LT006, а не сырая ошибка LT000). Без исправлений тесты падают — проверено откатом.
682
+ - Подключение по ключу (`connect({ apiKey })`, ключ сервиса) один раз обменивало ключ на сессию со сроком 7 суток и
683
+ больше его не обменивало: после недели простоя процесс получал `no_session` на запись и навсегда, пока его не
684
+ перезапустят, а чтение под истёкшей сессией шло как анонимное и молча отдавало пустые ответы. Так было у `svc` всех
685
+ четырёх сервисов-примеров. Теперь после простоя дольше минуты подключение до операции продлевает сессию и узнаёт,
686
+ жива ли она: по ключу — обменивает ключ заново (один обмен на все ждущие вызовы), с тем же рабочим арендатором, что
687
+ выбрал `auth.switch()`, и повторяет операцию (`runTx`, `db.begin()`); по токену — `no_session` на любую операцию
688
+ вместо пустого чтения. Ошибка продления другого рода (реплика только для чтения) работе не мешает, как прежде;
689
+ `refreshSession: false` — без проверки. Источник: сервис.
690
+ - `up()` от установщика без суперпользователя (роль с `CREATEROLE` и правом `create` на базе — поддержанный случай)
691
+ падал на `ANALYZE` с 42501 «permission denied for schema»: таблицы принадлежат роли владельца, а установщик состоит в
692
+ ней без наследования прав. С правом `usage` на схему PostgreSQL пропускал бы таблицы с WARNING, а `analyzed` называл
693
+ бы их собранными. Теперь `ANALYZE` идёт транзакцией от роли владельца, как у загрузчика; в `analyzed` — только
694
+ таблицы, где статистика появилась, оставшиеся без неё — предупреждение в `warnings`. `up({ fresh: true })` от такого
695
+ установщика падал «must be owner of schema» — схему теперь удаляет роль владельца. Источник: ревью.
696
+ - `npm run prerelease -- --only=…` и `-- --skip=…` затирали таблицу итогов в `docs/checkpoint-<x.y.z>.md`: в ней
697
+ оставались только шаги последнего запуска, хотя после падения повторяют упавший шаг и следующие. Теперь таблица
698
+ копится: строки шагов запуска заменяются, строки остальных остаются из прежних прогонов с датой и коммитом своего
699
+ прогона (колонка «Прогон»), шапка перечисляет прогоны, чьи строки в таблице; таблица прежнего вида читается как один
700
+ прогон. Запуск без шагов итог не пишет. Тест — `prerelease` (план 2.0, 7.8 №3).
701
+ - `node scripts/gen-api-contract.mjs --check` (его зовёт `check:docs`) и `guide/scripts/gen-api.ts --check` сравнивали
702
+ файлы побайтно и на Windows с `core.autocrlf=true` были красными на актуальных `docs/api-contract.json` и страницах
703
+ справочника сайта (CRLF в рабочей копии). Теперь сравнение — по LF, а `gen-api-contract.mjs` пишет файл с концами
704
+ строк самого файла.
705
+ - Загрузчик проверял шаблоны `pattern` (их процесс не исполняет — ревью, ReDoS) запросом к базе с `class_row` и полным
706
+ `validate_data` на каждую строку, в режиме истории — на каждую версию: засевка полигона salon (980 180 строк-версий)
707
+ шла на 42 % медленнее 1.0.1, 40 % времени загрузки — эта проверка (замер против 1.0.1). Теперь одинаковые класс и данные
708
+ проверяются один раз, строки классов читаются один раз на пачку (`materialized`), а полный разбор `validate_data`
709
+ идёт только у строк, которых не пропустил общий `validator` класса (правил `extra` в нём нет, но сюда доходят строки,
710
+ прошедшие валидатор на TypeScript, — у него те же правила, кроме `pattern`). Нарушения и первые 100 строк отчёта —
711
+ прежние, шаблоны по-прежнему исполняет только база. Засевка — 5671–5682 строки/с против 6245–6613 у 1.0.1 (−9…−14 %
712
+ в двух прогонах, было −42 %). Источник: тест.
713
+ - Чтение по одному id (по ключу или по id) собиралось как `id = any ($n::uuid[])`. Общий план подготовленного запроса
714
+ для `= any` PostgreSQL считает на ~10 элементов, и при неудачной выборке ANALYZE запрос навсегда оставался на планах
715
+ под каждый вызов — 0,5–0,8 мс планирования на чтение; та же ловушка у 1.0.1. Теперь один id — `id = $n::uuid`: общий
716
+ план стоит как план вызова и выбирается всегда (−18 % на схеме, где запрос застревал). «+27,7 %» замера против 1.0.1
717
+ «чтение по ключу» (прогон 2 ноября) был перебросом кэша планов между сторонами, а не кодом. Источник: тест.
718
+ - Подписка, закрытая (`close()`, `return()`), пока шёл её запрос журнала, не завершала `for await`: `close()` будит только
719
+ ждущий опрос, а после запроса цикл засыпал до будильника, которого уже не будет — таймер снят, сигналы отписаны. Так
720
+ поток SSE сервисов-примеров не закрывался после выхода. Теперь перед ожиданием подписка проверяет, не закрыта ли она.
721
+ Тест — `watch-contract` (план 2.0, 7.4 №8: запрос держит блокировка журнала, пока подписку закрывают; без исправления
722
+ цикл висит). Источник: сервис.
723
+ - Сервисы-примеры после обхода в браузере (сценарии `CHECK.md` четырёх сервисов через chrome-devtools-mcp, 2 ноября;
724
+ ядро не менялось). В кошельке выписка после «Перестроить выписку» шла в порядке строк таблицы — теперь по месяцам
725
+ (новые сверху) и номерам счетов. Время версий в
726
+ салоне и кошельке («История брони», «Последние операции», «История баланса», проход проектора) — местное, как в
727
+ «Под капотом»: сервер отдаёт `at` целиком, а не подстроку UTC. В helpdesk помощник считал открытыми только `new` и
728
+ `open` (счётчик и очередь — ещё `pending`); обновление очереди по событию сворачивало догруженные «Показать ещё»
729
+ страницы — теперь `GET /api/tickets?limit=N` перечитывает столько строк, сколько показано; у поля ответа и флажка
730
+ заметки — `id` и `name`; после отключения второго фактора текст «все сессии аккаунта закрыты» (гашение креда, который
731
+ не ключ, закрывает все сессии аккаунта, кроме сессии сервиса), так же — страница helpdesk сайта. Диалоги четырёх
732
+ сервисов (`components/ui/dialog.tsx`) снимают фокус с открывшей кнопки до `aria-hidden` Base UI и возвращают его после
733
+ закрытия (`finalFocus`) — предупреждение Chrome «Blocked aria-hidden on an element because its descendant retained
734
+ focus». `CHECK.md`: проверка — на свежем стенде (`down -v`); в кошельке чужая транзакция держит горизонт не дольше
735
+ 30 с; в helpdesk запись «Под капотом» об отключении второго фактора остаётся в закрытой сессии. Сценарии —
736
+ проверки полного `at` (кошелёк, салон), порядка выписки и `limit` очереди; без исправлений они
737
+ падают — проверено откатом. Повторный обход: поток событий SSE подписывался через фасад сервиса и переживал выход —
738
+ вкладка получала события прежнего салона, а с cookie новой сессии того же браузера показывала её данные под прежней
739
+ шапкой; теперь веб-сессия закрывает свои потоки (`services/shared/sessions.mjs`, `events.mjs`), а интерфейсы при
740
+ обрыве потока заново спрашивают `/api/me` и, если вошёл другой или никто, перезагружают страницу (проверка сценария
741
+ салона «выход закрывает поток событий своей сессии», без исправления падает). Строки очереди helpdesk и реестра
742
+ «Документов» фокусируемы (`tabIndex`, Enter и пробел открывают карточку): карточка открывается с клавиатуры, фокус
743
+ после закрытия возвращается на строку. Уход со страницы (переход на `/logout`) закрывает поток сразу — перезагрузка по
744
+ обрыву потока больше не перебивает начатый переход.
745
+
7
746
  ## [1.4.2] — 2026-11-01
8
747
 
9
748
  План 2.0 (`reports/PLAN-LETOPIS-2.0.md`): правила версий, проверка перед выпуском, исправления.