letopis 1.4.1 → 1.4.2

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.
@@ -194,6 +194,9 @@ await db.Wallet(or({ number: 'W-1' }, { number: 'W-200' })).ids()
194
194
  `{ Shop: id }`, `{ Shop: null }`. Fields are in `row.data`, not at the root of the row.
195
195
  - A step is a class name, an alias or an end role; a step by a parent class also returns descendants (`.exact()` —
196
196
  only its own class); `.deep(max)` — a tree (`$depth`); `.tags()`, `.owner()`, `.alias(name)`, `db.entity(row)`.
197
+ - `after(cursorOf(row, field))` requires `sort()` by the same field; it pages `rows()`, `versions()` and `paths()` (the
198
+ cursor of a path is `cursorOf(path, field)`, the keys of a path follow the order of the steps). `count()`,
199
+ aggregates, `diff()`, `blame()` ignore the cursor.
197
200
 
198
201
  ### 6. Deletion with a preview
199
202
 
@@ -352,7 +355,7 @@ they drop out. A tenant with a source with `history: false` drops out (`invalid_
352
355
  `withBefore`) is the previous version of the same id or `null`: `null` for a creation, for the first version of a new
353
356
  id after `reclass` and `rekey` (the tombstone of the former id subtracts the former data), during a rebuild and when
354
357
  the previous version is a deletion (after `restore`) or lies outside the `from` family; `purge`, `trim` and `reset`
355
- have no such field, so subtract `before` only after checking it for `null`. A class change that keeps the id (a class
358
+ have no such field (`undefined`), so subtract `before` only after checking that it is truthy: `if (c.before)`. A class change that keeps the id (a class
356
359
  without a key) and takes the row out of the `from` family comes as a deletion (`op` `delete`, `row` is the last
357
360
  version in the family with `$deleted`). `handle` gets the changes of one tenant. An error of its data (the
358
361
  database rejected its results, or `out` rejected its row or delta) rolls back only this tenant: it drops out
@@ -407,7 +410,7 @@ data, not instructions); `data` by terminal: `rows` — `{ rows, count, more, cu
407
410
  `{ ids, count, more, cursor? }` (`count` is the rows on the page), `first` — `{ row }`, `count` — `{ count }`,
408
411
  `countBy` — `{ counts, truncated? }`, `sum`, `avg`, `min`, `max` —
409
412
  `{ value }`. A refusal is `isError: true` with a `structuredContent` of the form `{ notice, error }` (`error` is
410
- `{ code, message, similar? }`). Writing only
413
+ `{ code, message, detail?, sqlstate?, similar? }`). Writing only
411
414
  with `write: true` (without it `letopis_write` is not in the list, and a call is an SDK protocol error): `update` with
412
415
  `rev`; `delete` without `confirm` is a preview, with `confirm` a human confirms it (elicitation), and without that it
413
416
  is `confirmation_required`. Data from the database in an answer sits inside `<untrusted-data-…>`.
@@ -187,6 +187,9 @@ await db.Wallet(or({ number: 'W-1' }, { number: 'W-200' })).ids()
187
187
  `{ Shop: null }`. Поля — в `row.data`, не в корне строки.
188
188
  - Шаг — имя класса, алиас или роль конца; шаг по родителю отдаёт и потомков (`.exact()` — только свой класс);
189
189
  `.deep(max)` — дерево (`$depth`); `.tags()`, `.owner()`, `.alias(имя)`, `db.entity(row)`.
190
+ - `after(cursorOf(строка, поле))` требует `sort()` по тому же полю; листает `rows()`, `versions()` и `paths()` (курсор
191
+ пути — `cursorOf(путь, поле)`, ключи пути — в порядке шагов). `count()`, агрегаты, `diff()`, `blame()` курсор не
192
+ учитывают.
190
193
 
191
194
  ### 6. Удаление с превью
192
195
 
@@ -342,7 +345,7 @@ p.run().catch(report) // run() без once — с catch: после ош
342
345
  Арендатор с источником `history: false` выбывает (`invalid_class`). `c.before` (с `withBefore`) —
343
346
  прошлая версия того же id или `null`: `null` при создании, у первой версии нового id после `reclass` и `rekey`
344
347
  (прежние данные вычитает надгробие прежнего id), при перестройке и когда прошлая версия — удаление (после `restore`)
345
- или вне семейства `from`; у `purge`, `trim` и `reset` поля нет — вычитайте `before`, только проверив его на `null`.
348
+ или вне семейства `from`; у `purge`, `trim` и `reset` поля нет (`undefined`) — вычитайте `before`, только проверив его истинность: `if (c.before)`.
346
349
  Смена класса без смены id (класс без ключа), уводящая строку из семейства `from`, приходит удалением (`op` `delete`,
347
350
  `row` — последняя версия в семействе с `$deleted`). `handle` получает изменения
348
351
  одного арендатора. Ошибку его данных (база отклонила его результаты или `out` — его строку или приращение)
@@ -392,7 +395,7 @@ const server = await createMcpServer(await svc.as(userId)) // letopis_model, l
392
395
  (`notice` — постоянное пояснение, что это данные, а не инструкции); `data` по терминалам: `rows` —
393
396
  `{ rows, count, more, cursor? }`, `ids` — `{ ids, count, more, cursor? }` (`count` — строк на странице), `first` —
394
397
  `{ row }`, `count` — `{ count }`, `countBy` — `{ counts, truncated? }`, `sum`, `avg`, `min`, `max` — `{ value }`.
395
- Отказ — `isError: true` и `structuredContent` вида `{ notice, error }` (`error` — `{ code, message, similar? }`). Запись — только `write: true`
398
+ Отказ — `isError: true` и `structuredContent` вида `{ notice, error }` (`error` — `{ code, message, detail?, sqlstate?, similar? }`). Запись — только `write: true`
396
399
  (без него `letopis_write` нет в списке, а вызов — ошибка протокола SDK): `update` с `rev`; `delete` без `confirm` —
397
400
  превью, с `confirm` — подтверждение человека (elicitation), без него — `confirmation_required`. Данные из базы в
398
401
  ответе — внутри `<untrusted-data-…>`.
package/CHANGELOG.md CHANGED
@@ -1,11 +1,150 @@
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
+ ## [1.4.2] — 2026-11-01
8
+
9
+ План 2.0 (`reports/PLAN-LETOPIS-2.0.md`): правила версий, проверка перед выпуском, исправления.
10
+
11
+ ### Добавлено
12
+ - Правила версий — подраздел «Версии и совместимость» в README (обе версии): патч — только исправления без новой
13
+ ревизии движка и без новых отказов; минор — новые возможности и новая ревизия с `up({ upgrade: true })`; мажор —
14
+ удаление, переименование, смена сигнатур и новые отказы.
15
+ - `npm run check:semver` (`scripts/check-semver.mjs`): контракт API и ревизия движка текущего дерева против тега
16
+ прошлого выпуска — исчезло → мажор, появилось или выросла ревизия → минор, тексты ошибок → патч; шаг версии обязан
17
+ быть не меньше. Входит в `check:docs` (в CI checkout с полной историей) и в `release.yml` до публикации. Сборка
18
+ контракта вынесена в `scripts/contract.mjs`; в `docs/api-contract.json` появилось поле `codes` — коды ошибок.
19
+ - `npm run prerelease` (`scripts/prerelease.mjs`) — полная локальная проверка одной командой, строго по очереди:
20
+ typecheck, сверка документов, весь `npm test`, примеры сайта со снимками, ER-схемы, покрытие, сборка сайта,
21
+ песочница, сценарии четырёх сервисов, проверка пакета с живым прогоном. Первый красный шаг останавливает прогон;
22
+ итог — таблица в `docs/checkpoint-<x.y.z>.md`. Тесты в CI не гоняются (решение владельца 01.11.2026).
23
+ - Долгий прогон `npm run soak` (`bench/soak.mjs`, план 2.0, этап 2): смешанная нагрузка одного процесса на часы —
24
+ писатели с агрегатом и `inc`, читатели, подписки, проектор, `maintain()` с прореживанием под записью, вход и выход,
25
+ `db.as()`, заведение и удаление классов; замер раз в 5 минут, в конце — критерии §7.7 и `verify({ data: true })`.
26
+ Быстрый режим на 15 минут — шаг `soak` в `npm run prerelease`.
27
+ - Метрика стабилизации: у правки ядра в CHANGELOG — источник находки, `release-notes.mjs` считает их в теле релиза.
28
+ - Тест `upgrade` №3: накат схемы 1.4.1 (dev-зависимость `letopis-141`) обычным `up()` без миграций; правило — каждый
29
+ выпуск проверяет накат с предыдущего. Тесты `semver` и `prerelease` проверяют сами сценарии (план 2.0, 7.8).
30
+ - `CONTRIBUTING.md` (и английская версия), шаблон issue об ошибке, форма отчёта о внедрении
31
+ `docs/adoption/TEMPLATE.md`.
32
+
33
+ ### Исправлено
34
+ - Строки-замки удалённых классов не убирались: запись строки `Class` заводит две строки `LOCK` в `secret` (замок
35
+ семейства и его шлюз), а удаление класса их оставляло — приложение, которое заводит и удаляет классы на лету, копило
36
+ их без предела. Теперь `maintain()` убирает строки-замки семейств, чей корневой класс удалён (или удалён его
37
+ арендатор); строку, которую держит транзакция, — следующим вызовом; класс с тем же именем заводит их заново
38
+ (7.7 №8). Источник: soak.
39
+ - `maintain()` мог безвозвратно стереть историю: если класс System заводили позже одноимённого класса арендатора,
40
+ строки журнала арендатора попадали в отбор дважды, и промежуточные версии ярусов стирались. Теперь политику выбирают
41
+ прореживание, `verify` и срок курсора подписки так же, как класс строки: свой класс арендатора, иначе System
42
+ (план 2.0, 7.3 №1). Источник: ревью.
43
+ - История удалённого объекта стирается целиком и тогда, когда до `reclass` он был в классе без политики: версии
44
+ прежнего класса больше не остаются и не показывают объект живым в срезе (7.3 №2). Источник: ревью.
45
+ - Стирание истории удалённого ждёт `restore()`, начатый в другой транзакции, и после него историю не трогает: раньше
46
+ цепочка восстановленного объекта начиналась не с версии 1 (7.3 №3). Источник: ревью.
47
+ - `maintain()` чистит истёкшие сессии и коды один раз за вызов, в первой пачке, а не в каждой из пачек (до 100):
48
+ у функции базы `maintain` третий аргумент `p_cleanup`; схема, которую ещё не обновил `up()`, получает прежний
49
+ вызов (7.3 №4). Источник: ревью.
50
+
51
+ - Два `create` одного ключа в батче проходили молча: обе строки ответа были одной и той же строкой. Теперь повтор —
52
+ `exists`, и батч откатывается целиком (7.2 №1). Источник: ревью.
53
+ - Строки ответа `reclass`, `restore` и `rekey` — в порядке целей (`sort()` до глагола, иначе id), как обещает README;
54
+ в 1.4.1 так стало только у `update` и `anonymize` (7.2 №2). Источник: ревью.
55
+ - Цепочка с глаголом и читающим хвостом в значении слота (`.Cat.add(db.Cat().create(…).Parent())`) падала с
56
+ `TypeError`; вложенный план не проходил проверки до базы. Теперь работает, а модификатор сразу после её глагола —
57
+ `invalid_query`, как у терминала (7.2 №3). Источник: ревью.
58
+ - Кэш результатов отдавал общие объекты: правка `rows[0].data` у ответа `.cache()` портила следующие ответы. Теперь
59
+ кэш хранит и отдаёт копии (7.2 №5). Источник: ревью.
60
+
61
+ - Подписка без событий опрашивала журнал сериями по 40 коротких повторов на каждый тик таймера — около 4 запросов в
62
+ секунду вместо одного за `pollMs`. Теперь короткие повторы идут только после сигнала (7.4 №1). Источник: ревью.
63
+ - `close()` подключения не закрывал живые подписки: с чужим пулом (`connect({ sql })`) они опрашивали журнал и после
64
+ `close()`. Теперь подписки закрываются, ожидающий `next()` получает `done: true` (7.4 №8). Источник: ревью.
65
+
66
+ - `reclass` класса без ключа (id тот же) сигналил только новый класс: ответы кэша и подписки прежнего класса не
67
+ сбрасывались и отдавали строку, которая из него ушла. Теперь сигнал идёт по обоим классам, как у пути через
68
+ триггер (7.4 №4). Источник: ревью.
69
+
70
+ - `letopis types` падал на кириллическом имени класса (`invalid_class`, хотя такой идентификатор в TypeScript допустим),
71
+ а для класса без полей, класса с `anyOf`/`oneOf` и открытой схемы выдавал неверный TypeScript
72
+ (`export interface XData Record<string, unknown>`). Теперь имена — буквы Unicode, не-объекты — `export type`, ветки
73
+ `anyOf` из одних `required` у класса с полями — условие, а не форма (7.5 №1). Источник: ревью.
74
+ - `hub('X', { fields: z.object(…) })` без других ключей описания читался как короткая форма — класс с единственным
75
+ полем `fields`. Теперь это полная форма (7.5 №2). Источник: ревью.
76
+ - Понятные ошибки вместо сырых: `connect({ owner })` с ролью, которой нет, — `invalid_data`; `anonymize(['опечатка'])` —
77
+ «поля нет в классе»; `up()` на готовой роли владельца без ADMIN OPTION — объяснение, что нужно; `auth.switch` при
78
+ имперсонации сырым SQL — `acl_denied` из каталога, а не неизвестный код; `--trust` — `LetopisError` (7.5 №3).
79
+ Источник: ревью.
80
+ - CLI: `letopis <команда> --help` возвращает код 0, неизвестная команда — справку с кодом 2 ещё до подключения к базе
81
+ (7.5 №4). Источник: ревью.
82
+ - Сравнение имени проектора в `88-derived.sql` — той же коллацией, что индекс `secret_kind_ident` (7.5 №5). Источник:
83
+ ревью.
84
+
85
+ - Срез `asOf` по классу и по концам показывал объект в классе, из которого он уже ушёл (`reclass` класса без ключа
86
+ сохраняет id): последняя версия выбиралась среди версий классов шага. Теперь она выбирается среди всех версий id, и
87
+ класс проверяется у неё — как уже делала ветка по id; то же в `trimHistory` по классам (7.1 №1). Источник: ревью.
88
+ - `after()` в `versions()` молча не действовал: страница «после курсора» повторяла первую. Теперь курсор листает версии
89
+ в порядке `sort()`. Курсор версии несёт её `rev` (`cursorOf` кладёт его сам), поэтому версии одного объекта с равным
90
+ полем сортировки не теряются. Без `sort()` — `invalid_query`, как у `rows()`. `count()`, агрегаты, `diff()` и
91
+ `blame()` курсор не учитывают, README теперь это описывает (7.1 №4). Источник: ревью.
92
+ - `paths()` с `after()` терял пути с общим последним узлом: курсор помнил только этот узел, и следующая страница
93
+ начиналась после всех путей к нему. Теперь `cursorOf(путь, поле)` кладёт в курсор id всех узлов (`path`), а пути
94
+ упорядочены по ним после поля и последнего узла. Ключи объекта пути идут в порядке шагов, как в примере README;
95
+ прежде они шли в порядке jsonb — по длине имени (7.1 №3). Источник: ревью.
96
+ - `versions({ follow: true })`, `diff({ follow: true })` и `blame()` после `rekey` смешивали историю перенесённого
97
+ объекта с объектом, которого потом завели на прежнем id (тот же ключ): брались все версии всех связанных id.
98
+ Теперь история идёт по отрезкам между переносами — надгробие переноса кончает отрезок, первая версия на новом id
99
+ начинает (функция базы `history_segments`, обычный `up()`); у нового объекта своя история с его создания (7.1 №2).
100
+ Источник: ревью.
101
+ - `blame()` на каждом новом соединении первые пять вызовов шли с квадратичной сложностью: свой план функции базы
102
+ `version_paths` по фильтру видимости оценивал версии в 2 строки и искал предыдущую версию по одному id, проверяя
103
+ номер фильтром, — около 250 мс на 1000 версиях и секунды на 10 000. Теперь обе версии ищутся по `(id, rev)` на
104
+ каждую пару: 21 мс с первого вызова (`bench/results/history.json`; ошибку нашёл замер `bench/history.bench.mjs`
105
+ при проверке 7.1 №2). Источник: тест.
106
+ - Проектор не строил данных арендатора, который стал участником после первой проекции: изменения шли только с
107
+ курсора, строки, заведённые до вступления, в проекцию не попадали. Теперь чекпоинт хранит состав участников
108
+ (`tenants`), и новый участник получает начальную перестройку пачками (7.4 №5). Источник: ревью.
109
+ - Проектор на подключении без LISTEN не перечитывал состав участников: сигнала о новых классах нет, и арендатор,
110
+ вступивший после запуска, не участвовал вовсе. Теперь состав читается в начале `run()` и не реже раза в 5 с (7.4 №6).
111
+ Источник: ревью.
112
+ - Реестры классов `db.as()` копились без предела: процесс, который ходит от имени многих арендаторов, держал реестр
113
+ каждого и перечитывал их все на каждый сигнал классов. Теперь их не больше 1000 на подключение, давно не нужный
114
+ вытесняется; фасад с вытесненным реестром работает — реестр возвращается и перечитывается (7.4 №8). Источник: ревью.
115
+ - Загрузчик с `existing: 'update'` писал в строку другого класса, если у неё тот же id: объект класса без ключа после
116
+ `reclass` сохраняет id, и повторный импорт по `$ext` правил его данными, проверенными по схеме прежнего класса, мимо
117
+ триггеров. Теперь такая строка — `exists` с классом объекта в тексте (7.2 №4). Источник: ревью.
118
+
119
+ - Имя поля, в котором есть `@S@` (например, `a@S@b`), портилось в запросе цепочки: построитель подставлял на это место
120
+ имя схемы, и фильтр, `sort()` или агрегат по такому полю ничего не находили (7.1 №5). Источник: ревью.
121
+
122
+ - После обрыва соединения транзакция проектора, `db.begin()` при открытии и загрузчик слали `rollback` в мёртвое
123
+ соединение и возвращали его в пул — postgres.js 3.4 ронял процесс необработанным `TypeError`. Теперь, как в
124
+ `runTx`: после обрыва в соединение не пишут и в пул его не возвращают, промис отклоняется (7.4 №7). Источник: ревью.
125
+
126
+ - `watch({ from: 'start' })` после первой полной пачки событий старше яруса `all` падал с `cursor_expired`, хотя
127
+ обещан весь журнал. Теперь, пока подписка не догнала журнал, свои чтения она шлёт без срока курсора; курсоры
128
+ событий и `sub.cursor` время хранят, и продолжение с них после прореживания — по-прежнему `cursor_expired` (7.4 №3).
129
+ Источник: ревью.
130
+
131
+ - Подписка на класс не получала события его потомков, заведённых после `watch()`: список потомков фиксировался при
132
+ подписке, и курсор уходил за их события. Теперь классы с потомками берутся из текущего реестра (7.4 №2).
133
+ Источник: ревью.
134
+
135
+ ### Изменено
136
+ - CI: у задания `test` нет сервиса PostgreSQL (живой прогон пакета в CI выключен, база не нужна), комментарии
137
+ `ci.yml` описывают то, что задания делают.
4
138
 
5
139
  ## [1.4.1] — 2026-11-01
6
140
 
7
141
  Исправления, найденные прогоном примеров документации для людей (`guide/`).
8
142
 
143
+ По правилам версий, введённым в 1.4.2, часть изменений этого выпуска требовала минора (ревизия движка 7 с миграцией
144
+ `0007`, новый метод `auth.session`), а новые отказы (`invalid_query` для модификатора после глагола, `acl_denied` для
145
+ удаления `Account` System, код 1 у `letopis types` и `letopis describe` без сессии) и убранный код `not_implemented` —
146
+ мажора.
147
+
9
148
  ### Добавлено
10
149
  - `db.auth.session(token)` — сведения о сессии по её токену для сервиса: чья сессия (`account`), в каком арендаторе
11
150
  она работает (`tenant`), цело ли там членство (`member`; в собственном арендаторе — всегда `true`), когда она
package/README.en.md CHANGED
@@ -392,11 +392,30 @@ class name. While the index is being built, writes to the journal wait. On a lar
392
392
  without stopping writes: `create index concurrently log_class_tenant_at on "v2.<schema>".log (class, tenant, at)` as
393
393
  the owner role — then `up()` only drops the old one.
394
394
 
395
- The `upgrade` test checks upgrading 1.1.0 and 1.0.1 schemas. The old package installs the schema and the old engine
395
+ The `upgrade` test checks upgrading 1.4.1, 1.1.0 and 1.0.1 schemas; every release checks the upgrade from the previous
396
+ release. The old package installs the schema and the old engine
396
397
  writes the data, then the current version's `up()` runs. After it, the versions and hashes written by the old engine
397
- are in place, `verify({ data: true })` is clean, and the schema catalog matches a clean installation. Both schemas are
398
- upgraded with `up({ upgrade: true })`: 1.1.0 (revision 6) with migration `0007`, 1.0.1 (revision 5) with migrations
399
- `0006` and `0007`.
398
+ are in place, `verify({ data: true })` is clean, and the schema catalog matches a clean installation. A 1.4.1 schema of the
399
+ same revision 7 is upgraded with a plain `up()`, without migrations; 1.1.0 (revision 6) and 1.0.1 (revision 5) — with
400
+ `up({ upgrade: true })`: with migration `0007` and with migrations `0006` and `0007`.
401
+
402
+ ### Versions and compatibility
403
+
404
+ letopis versions follow semver. What each version step promises:
405
+
406
+ - **Patch** (1.4.1 → 1.4.2) — fixes only. The engine revision stays the same: a plain `up({ schema })` re-applies the
407
+ fixed functions, there are no migrations; until it runs, `connect()` warns about the stamp mismatch. Working code gets
408
+ no new refusals, and no methods, options, exports or error codes disappear.
409
+ - **Minor** (1.4 → 1.5) — new features: methods, options, error codes; behavior changes that do not break working code;
410
+ a new engine revision. `up({ schema, upgrade: true })` applies the new revision. Nothing is removed or renamed.
411
+ - **Major** (1.x → 2.0) — removed and renamed methods, options, exports and error codes, changed signatures and types,
412
+ new refusals for everyone. `MIGRATION.md` describes what to change in your code.
413
+
414
+ The text of an error message is not part of the API: a patch may make it more precise. Rely on `err.code`.
415
+
416
+ Before every release the `check:semver` script checks the version step: it compares the API contract
417
+ (`docs/api-contract.json`) and the engine revision with the previous release and requires at least the matching step.
418
+ Every release checks the schema upgrade from the previous release (the `upgrade` test).
400
419
 
401
420
  ### Development without PostgreSQL: PGlite
402
421
 
@@ -917,8 +936,8 @@ const page2 = await db.Product().sort('data.price', 'desc')
917
936
 
918
937
  `sort(field, 'asc' | 'desc')` sorts by `'at'`, `'rev'`, `'id'` or `'data.<path>'`. `after(cursorOf(row, field))` is
919
938
  pagination "after this row"; it requires `sort()`. Such pagination withstands inserts and deletes between pages, but a
920
- row whose sort field has changed may drop out or repeat. A cursor is an object `{ v, id }` (the field value and the row
921
- id): it survives JSON, so you can hand it to a client. The field in `cursorOf` must match the `sort` field. The cursor
939
+ row whose sort field has changed may drop out or repeat. A cursor is an object `{ v, id, rev }` (the field value, the
940
+ row id and its version number): it survives JSON, so you can hand it to a client. The field in `cursorOf` must match the `sort` field. The cursor
922
941
  value is checked against the type of the `sort` field before the query, and a wrong one is `invalid_query`: a numeric
923
942
  field needs a finite number, `rev` an integer, a boolean field `true` or `false`, a text field a string, uuid fields
924
943
  and `id` a uuid, time fields and `at` an ISO string with a time zone, a date field `YYYY-MM-DD`, a field without a
@@ -926,6 +945,12 @@ description a string, a number or `true`/`false`. `null` fits only data fields,
926
945
  The check cannot tell apart a cursor of the same type taken from another field. `limit` and `offset` are available
927
946
  too.
928
947
 
948
+ `after()` also pages `versions()`: the cursor of a version carries its `rev`, so versions of one object with an equal
949
+ sort field are not lost. In `paths()` the cursor comes from `cursorOf(path, field)`: the field and the id are those of
950
+ the last node of the path, and `path` holds the ids of all nodes, so paths to the same node are not lost. A cursor
951
+ `cursorOf(path.Node, field)` taken from a single node skips the other paths to that node. `count()`, aggregates,
952
+ `diff()` and `blame()` ignore the cursor.
953
+
929
954
  A data field may be `null` or missing. `sort` places such rows the way `order by` in PostgreSQL does: last when
930
955
  ascending, first when descending, and `after()` treats `null` as a value after all others (ascending) or before them
931
956
  (descending). So pages go through such rows without gaps or repeats, and a cursor with the value `null` gives the next
@@ -964,7 +989,8 @@ const orderPaths = await db.Order(order).line().Product().paths() // [{ Order:
964
989
  ```
965
990
 
966
991
  `paths()` returns the nodes of every variant of the path; the key of a node is the step name or the name from
967
- `.alias(name)`. Repeating a link in the chain returns to its node: this way you can go from one link in several
992
+ `.alias(name)`. The keys follow the order of the steps; if the chain ends by returning to an earlier node, that node
993
+ comes last. Repeating a link in the chain returns to its node: this way you can go from one link in several
968
994
  directions. `db.entity(row | chain)` inserts a ready node into the path.
969
995
 
970
996
  ## 7. Deleting and restoring
@@ -1277,7 +1303,7 @@ const changed = await adm.load({ Shop: [{ name: 'Cornflower', city: 'Samara' }]
1277
1303
  | Option | Values |
1278
1304
  |---|---|
1279
1305
  | `duplicates`: a repeated id within the load | `'error'` (default): cancel; `'first'` or `'last'`: keep the first or the last row |
1280
- | `existing`: the object already exists in the database | `'error'` (default): cancel; `'skip'`: skip, do not resurrect deleted objects; `'update'`: a new version if the content differs (unchanged objects are counted in `skipped`), deleted objects are created again |
1306
+ | `existing`: the object already exists in the database | `'error'` (default): cancel; `'skip'`: skip, do not resurrect deleted objects; `'update'`: a new version if the content differs (unchanged objects are counted in `skipped`), deleted objects are created again; an object of another class with the same id (a class without a key after `reclass`) is `exists` |
1281
1307
 
1282
1308
  ### Portions, history and verification
1283
1309
 
@@ -1486,8 +1512,9 @@ A service acts on behalf of a user through `svc.as(account, { tenant, owner })`.
1486
1512
  `auth.impersonate` permission (by System rules), that the user exists and is enabled, and that the tenant is available
1487
1513
  to the user, either their own or through membership. You cannot act on behalf of System, services, system
1488
1514
  administrators or disabled accounts. The user's visibility under the access rules is not checked. In the journal,
1489
- `author` is the user and `agent` is the service. A user's connection by their own token switches between tenants like
1490
- this:
1515
+ `author` is the user and `agent` is the service. The class registry of `db.as()` is one per tenant, and a connection
1516
+ keeps at most 1000 of them: a long unused one is evicted, and a facade whose registry was evicted keeps working — the
1517
+ registry is re-read. A user's connection by their own token switches between tenants like this:
1491
1518
 
1492
1519
  <!-- run: guide -->
1493
1520
  ```ts
@@ -1622,6 +1649,12 @@ Without `sort()`, versions go by id and version number (with `follow`, by time).
1622
1649
  too, and `limit()` limits them in the query itself: `db.Product(kettle).sort('at', 'desc').limit(5).versions()` gives
1623
1650
  the five latest versions, newest first.
1624
1651
 
1652
+ `follow: true` gives the history of the object across all its ids: before a move, the versions of the former id;
1653
+ after it, of the new one. An object created later on the former id (the same key after `rekey`) is a different one,
1654
+ and its versions are not in this history; its own history with `follow` starts at its creation. `diff({ follow: true
1655
+ })` and `blame()` work the same way. Without `follow`, `versions()` gives every version of the id, whoever it
1656
+ belonged to.
1657
+
1625
1658
  ### Point-in-time read
1626
1659
 
1627
1660
  <!-- run: guide -->
@@ -1724,7 +1757,8 @@ none.
1724
1757
  single-valued end, every id of a multi-valued end (`['links', role, id]`), every tag (`['tags', tag]`), `owner` and
1725
1758
  `class`. `rev`, `at`, `author`, `agent`, `op` and `reason` come from the version that changed the leaf last. Empty
1726
1759
  `agent` and `reason` are omitted; `id` is present only if it is a version of the former id (the history went
1727
- through `reclass` or `rekey`).
1760
+ through `reclass` or `rekey`). `blame()` does not look at versions of another object on the same id — one that
1761
+ lived there before a move or was created after it.
1728
1762
  - `exact: false` means there are gaps between the current version and the found one (the history is thinned,
1729
1763
  trimmed or partly invisible): a missing version may have changed the leaf too. `complete: true` means the whole
1730
1764
  history was scanned without gaps; by default at most the 1000 latest versions are scanned.
@@ -1943,6 +1977,10 @@ sales.close() // stop() after the current bat
1943
1977
  not see the changes of a class with `history: false`: it has no journal. So a tenant with such a class among the
1944
1978
  sources drops out with the code `invalid_class`, and the projector does not build its derived rows. Turn the journal
1945
1979
  of the class on and rebuild the tenant with `rebuild({ tenant })`.
1980
+ - A tenant that became a participant after the first projection (the administrator marked its classes later) gets an
1981
+ initial batch rebuild: the rows created before it joined get into the projection too. A projector re-reads its
1982
+ participants on a class signal, and on a connection without LISTEN (`listen: false`) at the start of `run()` and at
1983
+ least every 5 seconds.
1946
1984
  - `batch` is how many changes go into a batch (500), `pollMs` is fallback polling (1000 ms): source signals wake the
1947
1985
  projector earlier.
1948
1986
  - `lockTimeout` is how long a projector transaction waits for another transaction's row lock (5000 ms; 0 — no limit).
@@ -2062,7 +2100,8 @@ const report = await adm.maintain() // { sessions, codes, thinned, batches,
2062
2100
 
2063
2101
  `maintain()` deletes sessions and one-time codes of all kinds (`OTP` and custom `OTP_<NAME>` from `issueOtp({ kind })`)
2064
2102
  from the `secret` table — expired ones, as well as
2065
- revoked sessions and codes that were used, replaced by a new one or ran out of attempts, — thins the journal by the
2103
+ revoked sessions and codes that were used, replaced by a new one or ran out of attempts, — and the lock rows of deleted
2104
+ classes (two per class; a row held by a transaction goes on the next call), thins the journal by the
2066
2105
  retention policy and checks the PostgreSQL signal queue: if it is more than half full, you get a warning. You do not
2067
2106
  need `maintain()` for the rows of a deleted account: the deletion itself erases them ([Deleting an account](#deleting-an-account)).
2068
2107
  The system administrator, a role with the `letopis.maintain` permission or an admin connection can call it. An external scheduler
@@ -2587,7 +2626,7 @@ behavior.
2587
2626
  **Platform**
2588
2627
 
2589
2628
  - PostgreSQL 18 or newer is required; there are no workarounds for 15–17. AWS RDS, Google Cloud SQL, Azure Flexible
2590
- Server and Neon support 18, while Supabase, as of October 2026, offers only 15 and 17, so 1.0 will not work there.
2629
+ Server and Neon support 18, while Supabase, as of October 2026, offers only 15 and 17, so letopis will not work there.
2591
2630
  - The database must be in UTF8 encoding (`casefold()` and the `pg_unicode_fast` collation). If a new major PostgreSQL
2592
2631
  version updates the Unicode tables, the unique index of credential identifiers is rebuilt with the `reindex` command.
2593
2632
  - PGlite has one superuser connection: no concurrent writers and no `LISTEN`, and RLS does not apply
@@ -2798,6 +2837,12 @@ behavior.
2798
2837
  This section is for those who develop letopis itself. Run: `cd lib && npm test`. The harness starts the
2799
2838
  `letopis-pg18` container (`postgres:18`, port 15433) by itself if the database does not respond; each test file works in its own schema `v2.t_<name>`.
2800
2839
 
2840
+ CI runs no tests: before a push and a release, everything that checks behavior runs as one command,
2841
+ `cd lib && npm run prerelease` — typecheck, the documentation check, all tests, the site examples against snapshots, the
2842
+ sandbox, the service scenarios, a quick soak run (15 minutes of mixed load) and the package check with a live run, in
2843
+ order; the first red step stops the run. The same load for a day: `cd lib && npm run soak` (duration — `SOAK_HOURS`,
2844
+ 24 hours by default). A test for a fix is named after its test-plan item ("plan 2.0, 7.3 No. 1") and fails without the fix.
2845
+
2801
2846
  ```
2802
2847
  # connect — connecting under a session, refusing roles that bypass RLS, the registry, the deferred signal, error translation (stage 2)
2803
2848
  # context — sessions, service sign-in by key, the RLS policies of stage 1 (stage 1)
@@ -2844,9 +2889,9 @@ This section is for those who develop letopis itself. Run: `cd lib && npm test`.
2844
2889
  # retention — retention policy: tiers, a finite term, special versions, deleted rows, asOf, cursor_expired, time zone, catch-up run, inheritance and default — versus a reference in TypeScript (stage 6)
2845
2890
  # watch — cursor subscription: commit order, a long transaction, resuming, listener drop, two tenants (stage 6)
2846
2891
  # cache — result cache: hits, own and others' writes, the key with the user, permissions, revoked sessions, listener drop, ttl, class marking, without LISTEN, a race, db.sql, the limit of signal counters (stage 6; plan 1.1, checkpoint A)
2847
- # maintain — maintenance: expired sessions and codes, permissions, one process under a lock, schedule (stage 6)
2892
+ # maintain — maintenance: expired sessions and codes, permissions, one process under a lock, schedule (stage 6), lock rows of deleted classes
2848
2893
  # up — the full installer: fresh installation, re-apply, custom seeds, automatic database creation, UTF8, version 17, upgrade with migrations, PGlite, the first tenant and user (up({ tenant }), createTenant) (stages 6, 8)
2849
- # upgrade — schema upgrade from 1.1.0 (upgrade: true, migration 0007) and from 1.0.1 (upgrade: true, migrations 0006 and 0007) with the letopis-11 and letopis-1 packages: versions and hashes written by the old engine, verify(), the catalog against a clean installation, lock rows, secret rows without an account erased, the application, a subscription and a projector over old data, a repeated up() (work after 1.2.0, stage 1; revision 7)
2894
+ # upgrade — schema upgrade from 1.4.1 (a plain up() without migrations, plan 2.0, 7.8 No. 2), from 1.1.0 (upgrade: true, migration 0007) and from 1.0.1 (upgrade: true, migrations 0006 and 0007) with the letopis-141, letopis-11 and letopis-1 packages: versions and hashes written by the old engine, verify(), the catalog against a clean installation, lock rows, secret rows without an account erased, the application, a subscription and a projector over old data, a repeated up() (work after 1.2.0, stage 1; revision 7)
2850
2895
  # scenario-salon — end-to-end salon scenario: 21 acts from 0.21 (salon) with a coverage matrix of the 1.0 API (stage 6)
2851
2896
  # telemetry — onQuery and slowMs, describe() and class metadata with inheritance, describeText (stage 6)
2852
2897
  # history — diff and blame over the log, the reason of a change for verbs, db.begin() and batches; a version hidden by an ACL condition, the changed column, a missing version, row_changes over 20 thousand targets (plan 1.1, stage 1 and checkpoint A)
@@ -2856,4 +2901,10 @@ This section is for those who develop letopis itself. Run: `cd lib && npm test`.
2856
2901
  # mcp — MCP tools: the model and meta only in results, tenant rights, the JSON chain and hints of similar names, row and byte limits with a cursor, the untrusted-data wrapper, writing only with write, update only with rev, deletion with a human confirmation (elicitation in revisions 2025 and 2026-07-28), the reason in blame, credentials, tool schemas, letopis without the SDK; letopis mcp over stdio, the HTTP handler; the confirmation window without the alias and meta, countBy and versions limited in the query itself, limits of steps and $or conditions; the permission classes only with allowAclWrite, a session in the System tenant only with allowSystemAdmin; structuredContent with a note, a requestState signature of the server user with an expiry (plan 1.1, stage 5; security review No. 2; checkpoint B and the work after it)
2857
2902
  # import — import from 0.21 with a fixture of a real 0.21: key violation, --keyless, per-class counters, verify, self-reference, repeat, ids without a key, a dangling reference, $$strict and fields starting with $, update, credentials, CLI (stage 7)
2858
2903
  # security-final — the final security review (stage 8, §7.10 No. 8.4): a family lock by the tenant of the root, account_purge without permission and restore/purge of a foreign id give neither a leak nor a lock; the functions of stage 6, log_refs_at, journal-based find_* reads and watch_read stay within their own tenant; NOTIFY carries only names or mode tokens; the cache key distinguishes the token, the actor and the tenant (stage 8)
2904
+ # semver — version rules: check:semver requires at least the matching version step — a major for a removed method or error code, a minor for a new method and a new engine revision, a patch for texts only (plan 2.0, 7.8 No. 1)
2905
+ # prerelease — the pre-release check: steps run strictly in order, the first red one stops the run and the following ones do not start (plan 2.0, 7.8 No. 3)
2906
+ # write-contract — README promises about writes (plan 2.0, 7.2): a repeated key in a batch gives exists and a rollback, the row order of reclass, restore and rekey responses, a chain with a verb as a slot value, the loader with existing: update does not edit a row of another class, a cached answer is not spoiled by editing an earlier answer
2907
+ # watch-contract — README promises about subscriptions and connections (plan 2.0, 7.4): an idle subscription polls the journal once per pollMs, close() of the connection closes live subscriptions, a reclass without a key resets the cache of the old class, a new projector participant gets an initial rebuild, a projector without LISTEN re-reads its participants, at most a limit of db.as() registries
2908
+ # model-contract — README promises about models, types, the CLI and errors (plan 2.0, 7.5): letopis types gives valid TypeScript for Cyrillic names, a class without fields and anyOf, the full form of hub() with z.object, clear errors instead of raw ones, CLI exit codes
2909
+ # read-contract — README promises about reads and history (plan 2.0, 7.1): an asOf slice by class and by ends after a class change without an id change, versions, diff and blame with follow after rekey, the after() cursor in versions() and paths(), a field name with @S@
2859
2910
  ```
package/README.md CHANGED
@@ -385,10 +385,30 @@ const adm = await connect({ dsn: ADMIN, schema: r.schema, token: r.tenant!.token
385
385
  записи: `create index concurrently log_class_tenant_at on "v2.<схема>".log (class, tenant, at)` от роли владельца, —
386
386
  тогда `up()` только снимет прежний.
387
387
 
388
- Обновление схем 1.1.0 и 1.0.1 проверяет тест `upgrade`. Схему ставит прежний пакет, данные пишет прежний движок, затем
388
+ Обновление схем 1.4.1, 1.1.0 и 1.0.1 проверяет тест `upgrade`; каждый выпуск проверяет обновление с предыдущего
389
+ выпуска. Схему ставит прежний пакет, данные пишет прежний движок, затем
389
390
  вызывается `up()` текущей версии. После него версии и хэши прежнего движка на месте, `verify({ data: true })` чист, а
390
- каталог схемы совпадает с чистой установкой. Обе схемы обновляются через `up({ upgrade: true })`: 1.1.0 (ревизия 6) —
391
- миграцией `0007`, 1.0.1 (ревизия 5) — миграциями `0006` и `0007`.
391
+ каталог схемы совпадает с чистой установкой. Схема 1.4.1 той же ревизии 7 обновляется обычным `up()`, без миграций;
392
+ 1.1.0 (ревизия 6) и 1.0.1 (ревизия 5) — через `up({ upgrade: true })`: миграцией `0007` и миграциями `0006` и `0007`.
393
+
394
+ ### Версии и совместимость
395
+
396
+ Версии letopis следуют semver. Что обещает каждый шаг версии:
397
+
398
+ - **Патч** (1.4.1 → 1.4.2) — только исправления. Ревизия движка та же: исправленные функции накатывает обычный
399
+ `up({ schema })`, миграций нет; до наката `connect()` предупреждает о расхождении штампа. Работающий код не получает
400
+ новых отказов, методы, опции, экспорты и коды ошибок не исчезают.
401
+ - **Минор** (1.4 → 1.5) — новые возможности: методы, опции, коды ошибок; изменения поведения, которые не ломают
402
+ работающий код; новая ревизия движка. Новую ревизию накатывает `up({ schema, upgrade: true })`. Удалённого и
403
+ переименованного нет.
404
+ - **Мажор** (1.x → 2.0) — удаление и переименование методов, опций, экспортов и кодов ошибок, смена сигнатур и типов,
405
+ новые отказы для всех. Что менять в коде, описывает `MIGRATION.md`.
406
+
407
+ Текст сообщения об ошибке в API не входит: в патче он может стать точнее. Полагайтесь на `err.code`.
408
+
409
+ Шаг версии перед каждым выпуском сверяет сценарий `check:semver`: он сравнивает контракт API (`docs/api-contract.json`)
410
+ и ревизию движка с прошлым выпуском и требует шаг не меньше нужного. Каждый выпуск проверяет обновление схемы с
411
+ предыдущего выпуска (тест `upgrade`).
392
412
 
393
413
  ### Разработка без PostgreSQL: PGlite
394
414
 
@@ -890,13 +910,18 @@ const page2 = await db.Product().sort('data.price', 'desc')
890
910
  `sort(поле, 'asc' | 'desc')` — по `'at'`, `'rev'`, `'id'` или `'data.<путь>'`. `after(cursorOf(строка, поле))` —
891
911
  постраничный вывод «после такой-то строки»; он требует `sort()`. Такой вывод устойчив к вставкам и удалениям между
892
912
  страницами, но строка, у которой изменилось поле сортировки, может выпасть или повториться. Курсор — объект
893
- `{ v, id }` (значение поля и id строки): он переживает JSON, и его можно отдать клиенту. Поле в `cursorOf` должно
913
+ `{ v, id, rev }` (значение поля, id и номер версии строки): он переживает JSON, и его можно отдать клиенту. Поле в `cursorOf` должно
894
914
  совпадать с полем `sort`. Значение курсора проверяется по типу поля `sort` до запроса, неверное — `invalid_query`:
895
915
  числовому полю нужно конечное число, `rev` — целое, логическому — `true` или `false`, текстовому — строка, uuid и
896
916
  `id` — uuid, времени и `at` — строка ISO с поясом, дате — `ГГГГ-ММ-ДД`, полю без описания — строка, число или
897
917
  `true`/`false`. `null` подходит только полям данных, `id` курсора — всегда uuid. Курсор того же типа, снятый
898
918
  с другого поля, проверка не отличит. `limit` и `offset` тоже есть.
899
919
 
920
+ `after()` листает и `versions()`: курсор версии несёт её `rev`, поэтому версии одного объекта с равным полем
921
+ сортировки не теряются. В `paths()` курсор даёт `cursorOf(путь, поле)`: поле и id — последнего узла пути, а в `path`
922
+ — id всех узлов, поэтому не теряются пути к одному узлу. Курсор `cursorOf(путь.Узел, поле)` с одного узла пропустит
923
+ остальные пути к этому узлу. `count()`, агрегаты, `diff()` и `blame()` курсор не учитывают.
924
+
900
925
  Поле данных бывает `null` или отсутствует. Такие строки `sort` ставит, как `order by` в PostgreSQL: при
901
926
  возрастании — в конце, при убывании — в начале, а `after()` считает `null` значением после всех прочих (по
902
927
  возрастанию) или перед ними (по убыванию). Поэтому страницы проходят такие строки без пропусков и повторов, а
@@ -934,7 +959,8 @@ const byStatus = await db.Order().countBy('data.status') // { new: 1 }
934
959
  const orderPaths = await db.Order(order).line().Product().paths() // [{ Order: Row, line: Row, Product: Row }, …]
935
960
  ```
936
961
 
937
- `paths()` отдаёт узлы каждого варианта пути; ключ узла — имя шага или имя из `.alias(имя)`. Повтор связи в
962
+ `paths()` отдаёт узлы каждого варианта пути; ключ узла — имя шага или имя из `.alias(имя)`. Ключи идут в порядке
963
+ шагов; если цепочка кончается возвратом к прежнему узлу, этот узел — последний. Повтор связи в
938
964
  цепочке возвращает к её узлу: так из одной связи можно пройти в несколько сторон. `db.entity(строка | цепочка)`
939
965
  вставляет готовый узел в путь.
940
966
 
@@ -1243,7 +1269,7 @@ const changed = await adm.load({ Shop: [{ name: 'Василёк', city: 'Сам
1243
1269
  | Опция | Значения |
1244
1270
  |---|---|
1245
1271
  | `duplicates` — повтор id внутри загрузки | `'error'` (по умолчанию) — отменить; `'first'` или `'last'` — оставить первую или последнюю строку |
1246
- | `existing` — объект уже есть в базе | `'error'` (по умолчанию) — отменить; `'skip'` — пропустить, удалённые не воскрешать; `'update'` — новая версия, если содержимое отличается (неизменённые считаются в `skipped`), удалённые — создать заново |
1272
+ | `existing` — объект уже есть в базе | `'error'` (по умолчанию) — отменить; `'skip'` — пропустить, удалённые не воскрешать; `'update'` — новая версия, если содержимое отличается (неизменённые считаются в `skipped`), удалённые — создать заново; объект другого класса с тем же id (класс без ключа после `reclass`) — `exists` |
1247
1273
 
1248
1274
  ### Порции, история и проверка
1249
1275
 
@@ -1442,7 +1468,8 @@ await boris.close()
1442
1468
  `auth.impersonate` (по правилам System), что пользователь существует и включён и что арендатор ему доступен —
1443
1469
  собственный или по членству. От имени System, сервисов, системных администраторов и отключённых аккаунтов
1444
1470
  действовать нельзя. Видимость пользователя по правилам доступа не проверяется. В журнале `author` — пользователь,
1445
- `agent` — сервис. Подключение
1471
+ `agent` — сервис. Реестр классов `db.as()` — один на арендатора, подключение держит их не больше 1000: давно не
1472
+ нужный вытесняется, а фасад, чей реестр вытеснен, продолжает работать — реестр перечитывается. Подключение
1446
1473
  пользователя по своему токену переключается между арендаторами так:
1447
1474
 
1448
1475
  <!-- run: guide -->
@@ -1571,6 +1598,11 @@ const followed = await db.Product(mug2).versions({ follow: true }) // и чер
1571
1598
  `limit()` ограничивает их в самом запросе: `db.Product(kettle).sort('at', 'desc').limit(5).versions()` — пять
1572
1599
  последних версий, новые первыми.
1573
1600
 
1601
+ `follow: true` отдаёт историю объекта на всех его id: до переноса — версии прежнего id, после — нового. Объект,
1602
+ который потом завели на прежнем id (тот же ключ после `rekey`), — другой, и его версий в этой истории нет; его
1603
+ собственная история с `follow` начинается с его создания. Так же идут `diff({ follow: true })` и `blame()`. Без
1604
+ `follow` `versions()` отдаёт все версии id, чьи бы они ни были.
1605
+
1574
1606
  ### Срез на дату
1575
1607
 
1576
1608
  <!-- run: guide -->
@@ -1667,7 +1699,8 @@ const diffed: DiffResult = {
1667
1699
  скаляр или массив в данных, пустой объект, роль одиночного конца, каждый id множественного конца
1668
1700
  (`['links', роль, id]`), каждый тег (`['tags', тег]`), `owner` и `class`. `rev`, `at`, `author`, `agent`, `op` и
1669
1701
  `reason` — от версии, которая последней изменила лист. Пустые `agent` и `reason` опускаются; `id` есть, только если
1670
- это версия прежнего id (история прошла через `reclass` или `rekey`).
1702
+ это версия прежнего id (история прошла через `reclass` или `rekey`). Версии другого объекта на том же id — того,
1703
+ что жил там до переноса или заведён после, — `blame()` не смотрит.
1671
1704
  - `exact: false` — между текущей версией и найденной есть пропуски (история прорежена, обрезана или часть версий не
1672
1705
  видна): лист мог менять и выпавшая версия. `complete: true` — история просмотрена целиком и без пропусков; по
1673
1706
  умолчанию просматривается не больше 1000 последних версий.
@@ -1870,6 +1903,10 @@ sales.close() // stop() после текущ
1870
1903
  из `into` источником не бывает, даже если попадает в `from`: свои результаты проектор не читает. Изменений класса
1871
1904
  с `history: false` проектор не видит: журнала у него нет. Поэтому арендатор, у которого такой класс среди
1872
1905
  источников, выбывает с кодом `invalid_class`, и его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
1906
+ - Арендатор, который стал участником после первой проекции (администратор пометил его классы позже), получает
1907
+ начальную перестройку пачками: в проекцию попадают и строки, заведённые до вступления. Состав участников проектор
1908
+ перечитывает по сигналу классов, а на подключении без LISTEN (`listen: false`) — в начале `run()` и не реже раза
1909
+ в 5 секунд.
1873
1910
  - `batch` — сколько изменений в пачке (500), `pollMs` — запасной опрос (1000 мс): сигналы источников будят
1874
1911
  проектор раньше.
1875
1912
  - `lockTimeout` — сколько транзакция проектора ждёт чужую блокировку строки (5000 мс; 0 — без предела). Транзакция
@@ -1983,7 +2020,8 @@ const report = await adm.maintain() // { sessions, codes, thinned, batches,
1983
2020
 
1984
2021
  `maintain()` удаляет из таблицы `secret` сессии и одноразовые коды всех видов (`OTP` и свои `OTP_<ИМЯ>` из
1985
2022
  `issueOtp({ kind })`) — истёкшие, а также погашенные сессии и коды, которые уже использованы, заменены новым или
1986
- исчерпали попытки, — прореживает журнал по политике хранения
2023
+ исчерпали попытки, — и строки-замки удалённых классов (по две на класс; строку, которую держит транзакция, — следующим
2024
+ вызовом), прореживает журнал по политике хранения
1987
2025
  и проверяет очередь сигналов PostgreSQL: заполнение больше половины — предупреждение. Строки удалённого аккаунта
1988
2026
  `maintain()` ждать не нужно: их стирает само удаление ([«Удалить аккаунт»](#удалить-аккаунт)). Вызывать может системный
1989
2027
  администратор, роль с правом `letopis.maintain` или администраторское подключение. Запускает его внешний
@@ -2491,7 +2529,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
2491
2529
  **Платформа**
2492
2530
 
2493
2531
  - Нужен PostgreSQL 18 или новее; обходных путей для 15–17 нет. AWS RDS, Google Cloud SQL, Azure Flexible Server
2494
- и Neon поддерживают 18, а Supabase на октябрь 2026 года — только 15 и 17, поэтому там 1.0 не заработает.
2532
+ и Neon поддерживают 18, а Supabase на октябрь 2026 года — только 15 и 17, поэтому там letopis не заработает.
2495
2533
  - База — в кодировке UTF8 (`casefold()` и коллация `pg_unicode_fast`). Если новая основная версия PostgreSQL
2496
2534
  обновит таблицы Unicode, уникальный индекс идентификаторов кредов перестраивается командой `reindex`.
2497
2535
  - PGlite — одно соединение суперпользователя: без параллельных писателей и `LISTEN`, RLS не действует
@@ -2689,6 +2727,12 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
2689
2727
  Раздел — для тех, кто развивает сам letopis. Запуск: `cd lib && npm test`. Обвязка сама поднимает контейнер `letopis-pg18` (`postgres:18`,
2690
2728
  порт 15433), если база не отвечает; каждый тестовый файл работает в своей схеме `v2.t_<имя>`.
2691
2729
 
2730
+ В CI тестов нет: перед пушем и выпуском всё, что проверяет поведение, идёт одной командой `cd lib && npm run prerelease` —
2731
+ по очереди typecheck, сверка документов, все тесты, примеры сайта со снимками, песочница, сценарии сервисов, быстрый
2732
+ долгий прогон (15 минут смешанной нагрузки) и проверка пакета с живым прогоном; первый красный шаг останавливает
2733
+ прогон. Та же нагрузка на сутки — `cd lib && npm run soak` (длительность — `SOAK_HOURS`, по умолчанию 24 часа). Тест
2734
+ исправления называется пунктом тест-плана («план 2.0, 7.3 №1») и без исправления падает.
2735
+
2692
2736
  ```
2693
2737
  # connect — подключение под сессией, отказ ролям в обход RLS, реестр, отложенный сигнал, перевод ошибок (этап 2)
2694
2738
  # context — сессии, вход сервиса по ключу, политики RLS этапа 1 (этап 1)
@@ -2735,9 +2779,9 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
2735
2779
  # retention — политика хранения: ярусы, конечный срок, особые версии, удалённые, asOf, cursor_expired, часовой пояс, догоняющий запуск, наследование и умолчание — против эталона на TypeScript (этап 6)
2736
2780
  # watch — подписка по курсору: порядок фиксации, долгая транзакция, возобновление, обрыв слушателя, два арендатора (этап 6)
2737
2781
  # cache — кэш результатов: попадание, свои и чужие записи, ключ с пользователем, права, погашенные сессии, обрыв слушателя, ttl, пометка класса, без LISTEN, гонка, db.sql, предел счётчиков сигналов (этап 6; план 1.1, точка А)
2738
- # maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6)
2782
+ # maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6), строки-замки удалённых классов
2739
2783
  # up — полный установщик: свежая установка, повторный накат, свои сиды, автосоздание базы, UTF8, версия 17, upgrade с миграциями, PGlite, первый арендатор и пользователь (up({ tenant }), createTenant) (этапы 6, 8)
2740
- # upgrade — обновление схемы с 1.1.0 (upgrade: true, миграция 0007) и с 1.0.1 (upgrade: true, миграции 0006 и 0007) пакетами letopis-11 и letopis-1: версии и хэши прежнего движка, verify(), каталог против чистой установки, строки-замки, строки secret без аккаунта стёрты, приложение, подписка и проектор по старым данным, повторный up() (доработки после 1.2.0, этап 1; ревизия 7)
2784
+ # upgrade — обновление схемы с 1.4.1 (обычный up() без миграций, план 2.0, 7.8 №2), с 1.1.0 (upgrade: true, миграция 0007) и с 1.0.1 (upgrade: true, миграции 0006 и 0007) пакетами letopis-141, letopis-11 и letopis-1: версии и хэши прежнего движка, verify(), каталог против чистой установки, строки-замки, строки secret без аккаунта стёрты, приложение, подписка и проектор по старым данным, повторный up() (доработки после 1.2.0, этап 1; ревизия 7)
2741
2785
  # scenario-salon — сквозной сценарий салона: 21 акт 0.21 (salon) с матрицей покрытия API 1.0 (этап 6)
2742
2786
  # telemetry — onQuery и slowMs, describe() и метаданные классов с наследованием, describeText (этап 6)
2743
2787
  # history — diff и blame по журналу, причина изменения у глаголов, db.begin() и батча; версия, скрытая условием ACL, колонка changed, пропуск версии, row_changes на 20 тыс. целей (план 1.1, этап 1 и точка А)
@@ -2747,4 +2791,10 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
2747
2791
  # mcp — инструменты MCP: модель и meta только в результатах, права арендатора, JSON-цепочка и подсказки похожих имён, предел строк и байт с курсором, обёртка недоверенных данных, запись только с write, update только с rev, удаление с подтверждением человека (elicitation в ревизиях 2025 и 2026-07-28), причина в blame, учётные данные, схемы инструментов, letopis без SDK; letopis mcp по stdio, обработчик HTTP; окно подтверждения без алиаса и meta, countBy и versions с пределом в самом запросе, предел шагов и условий $or; классы прав — только с allowAclWrite, сессия в арендаторе System — только с allowSystemAdmin; structuredContent с пояснением, подпись requestState пользователя сервера со сроком (план 1.1, этап 5; ревью безопасности №2; точка Б и доработки после неё)
2748
2792
  # import — импорт из 0.21 по фикстуре настоящей 0.21: нарушение ключа, --keyless, счётчики по классам, verify, самоссылка, повтор, id без ключа, висячая ссылка, $$strict и поля на $, update, креды, CLI (этап 7)
2749
2793
  # security-final — итоговое ревью безопасности (этап 8, §7.10 №8.4): блокировка семейства по арендатору корня, account_purge без права и restore/purge чужого id не дают ни утечки, ни блокировки; функции этапа 6, log_refs_at, срезы find_* по журналу и watch_read не выходят за свой арендатор; NOTIFY несёт только имена или токены режима; ключ кэша различает токен, актора и арендатора (этап 8)
2794
+ # semver — правила версий: check:semver требует шаг версии не меньше нужного — мажор за исчезнувший метод или код ошибки, минор за новый метод и новую ревизию движка, патча хватает на тексты (план 2.0, 7.8 №1)
2795
+ # prerelease — проверка перед выпуском: шаги идут строго по очереди, первый красный останавливает прогон, следующие не запускаются (план 2.0, 7.8 №3)
2796
+ # write-contract — обещания README о записи (план 2.0, 7.2): повтор ключа в батче — exists и откат, порядок строк ответа reclass, restore и rekey, цепочка с глаголом в значении слота, загрузчик с existing: update не правит строку другого класса, ответ из кэша не портится правкой прошлого ответа
2797
+ # watch-contract — обещания README о подписке и подключении (план 2.0, 7.4): молчащая подписка опрашивает журнал раз в pollMs, close() подключения закрывает живые подписки, reclass без ключа сбрасывает кэш прежнего класса, новый участник проектора получает начальную перестройку, проектор без LISTEN перечитывает состав, реестров db.as() не больше предела
2798
+ # model-contract — обещания README о моделях, типах, CLI и ошибках (план 2.0, 7.5): letopis types — верный TypeScript для кириллицы, класса без полей и anyOf, полная форма hub() с z.object, понятные ошибки вместо сырых, коды выхода CLI
2799
+ # read-contract — обещания README о чтении и истории (план 2.0, 7.1): срез asOf по классу и по концам после смены класса без смены id, versions, diff и blame с follow после rekey, курсор after() в versions() и paths(), имя поля с @S@
2750
2800
  ```