letopis 1.0.1 → 1.1.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.
@@ -1,4 +1,4 @@
1
- # letopis 1.0 — agent cheatsheet
1
+ # letopis 1.1 — agent cheatsheet
2
2
 
3
3
  One page instead of [README.en.md](README.en.md), for an AI agent that writes code on top of the package. It ships in
4
4
  the package next to the README; in Russian: [Русская версия](AGENT-CHEATSHEET.md). Migration from 0.21:
@@ -41,6 +41,12 @@ const r = await up({ dsn: ADMIN, schema: 'myapp', // schema 'v2
41
41
  policy), `allowReset`, `serviceKey`, `quiet`, `pglite: true` (a database for development without PostgreSQL; then
42
42
  `connect({ dsn: r.pglite.dsn, listen: false, max: 1, allowBypassRls: true, … })` — a superuser connection, RLS
43
43
  does not apply on it).
44
+ - `signals` — what `NOTIFY` signals show to any role that can connect to the database: `'class'` (default — class
45
+ names), `'tenant'` (the class name and a tenant token; the cache is cleared only in the tenant of the write),
46
+ `'opaque'` (tokens only), `'none'` (no signals: the cache works only with `ttlOnly`, subscriptions poll the log).
47
+ The mode is stored in the schema: `up()` without the option keeps it; after a change, restart the processes. The
48
+ signal key (precise cache clearing) goes to services — the `letopis.signals` permission. The `up()` result also has
49
+ `signals` and `warnings` (PostgreSQL below 18.2 — upgrade).
44
50
  - `tenant` — the first tenant and user without a seed of your own: the tenant gets the rule "authenticated users may
45
51
  do everything" (`grantAll`, on by default, weight 10); the user gets a membership with the roles `user.roles`
46
52
  (`['owner']` by default), a password for signing in with the login `login`, and a session in the tenant. Repeating
@@ -203,10 +209,14 @@ const rep = await adm.load({
203
209
  }, { existing: 'skip' }) // duplicates 'error'|'first'|'last', existing 'error'|'skip'|'update', commitEvery, withHistory, verify, tenant, as
204
210
  // rep: { rows, loaded, updated, skipped, duplicates, byClass, external, portions, ms }
205
211
  ```
212
+ Initial load: `adm.load(rows, { initial: true })`, `letopis load --initial`, `letopis import --initial` — only into an
213
+ empty schema (otherwise `not_empty`), in one transaction; about 2.2 times faster than a regular load on a million
214
+ objects; no `commitEvery`, `existing` or `{ as }`.
215
+
206
216
  NDJSON files (one object per line; all files form one load):
207
217
  ```sh
208
218
  npx letopis load shops.ndjson wallets.ndjson --dsn "$ADMIN" --schema v2.myapp --token "$TOKEN" [--class Wallet] \
209
- [--duplicates first] [--existing skip] [--commit-every 100000] [--verify]
219
+ [--duplicates first] [--existing skip] [--commit-every 100000] [--verify] [--initial]
210
220
  # {"$class":"Shop","name":"South"}
211
221
  # {"$class":"Wallet","number":"W-10","Shop":{"$key":["South"]}}
212
222
  # {"$class":"Note","text":"x","$ext":"n1"} ← $ext is the row's external key for references { "$ext": "n1" }
@@ -272,6 +282,8 @@ const was = await db.Wallet(w).asOf('2026-10-01T00:00:00Z').first() // as it w
272
282
  await db.Shop(shop).Wallet().asOf(new Date('2026-10-01')).rows()
273
283
  const history = await db.Wallet(w).versions() // all versions; the deletion version has $deleted
274
284
  await db.Wallet(w200).versions({ follow: true }) // also across id changes (reclass, rekey)
285
+ await db.Wallet(w).diff() // what changed: previous version → latest
286
+ await db.Wallet(w).blame() // who, when and why (verb option reason) changed fields
275
287
  ```
276
288
  A class with `history: false` does not provide `versions`, `asOf`, `restore` or subscription events.
277
289
 
@@ -1,4 +1,4 @@
1
- # letopis 1.0 — шпаргалка для агента
1
+ # letopis 1.1 — шпаргалка для агента
2
2
 
3
3
  Одна страница вместо [README.md](README.md) — для ИИ-агента, который пишет код поверх пакета. Лежит в пакете рядом с
4
4
  README; английская версия — [AGENT-CHEATSHEET.en.md](AGENT-CHEATSHEET.en.md). Переход с 0.21 — [MIGRATION.md](MIGRATION.md);
@@ -38,6 +38,12 @@ const r = await up({ dsn: ADMIN, schema: 'myapp', // схема
38
38
  `'booking'` — демо-домен, или путь к своему `.sql`), `history` (политика хранения), `allowReset`, `serviceKey`,
39
39
  `quiet`, `pglite: true` (база для разработки без PostgreSQL; затем `connect({ dsn: r.pglite.dsn, listen: false,
40
40
  max: 1, allowBypassRls: true, … })` — подключение суперпользователя, RLS на нём не действует).
41
+ - `signals` — что видно в сигналах `NOTIFY` любой роли, способной подключиться к базе: `'class'` (по умолчанию —
42
+ имена классов), `'tenant'` (имя класса и токен арендатора; кэш сбрасывается только в арендаторе записи),
43
+ `'opaque'` (только токены), `'none'` (сигналов нет: кэш — только `ttlOnly`, подписка опрашивает журнал). Режим
44
+ хранится в схеме: `up()` без опции его не меняет; после смены — перезапуск процессов. Ключ сигналов (точный сброс
45
+ кэша) получают сервисы — право `letopis.signals`. Результат `up()`: ещё `signals` и `warnings` (PostgreSQL ниже
46
+ 18.2 — обновить).
41
47
  - `tenant` — первый арендатор и пользователь без своего сида: арендатор получает правило «аутентифицированным можно
42
48
  всё» (`grantAll`, по умолчанию да, вес 10), пользователь — членство с ролями `user.roles` (по умолчанию `['owner']`),
43
49
  пароль для входа по логину `login` и сессию в арендаторе. Повтор с тем же именем и логином ничего не дублирует и роли
@@ -197,10 +203,14 @@ const rep = await adm.load({
197
203
  }, { existing: 'skip' }) // duplicates 'error'|'first'|'last', existing 'error'|'skip'|'update', commitEvery, withHistory, verify, tenant, as
198
204
  // rep: { rows, loaded, updated, skipped, duplicates, byClass, external, portions, ms }
199
205
  ```
206
+ Первая заливка: `adm.load(rows, { initial: true })`, `letopis load --initial`, `letopis import --initial` — только в пустую
207
+ схему (иначе `not_empty`), одной транзакцией; на миллионе объектов — примерно в 2,2 раза быстрее обычной загрузки;
208
+ без `commitEvery`, `existing` и `{ as }`.
209
+
200
210
  Файлы NDJSON (объект на строку, все файлы — одна загрузка):
201
211
  ```sh
202
212
  npx letopis load shops.ndjson wallets.ndjson --dsn "$ADMIN" --schema v2.myapp --token "$TOKEN" [--class Wallet] \
203
- [--duplicates first] [--existing skip] [--commit-every 100000] [--verify]
213
+ [--duplicates first] [--existing skip] [--commit-every 100000] [--verify] [--initial]
204
214
  # {"$class":"Shop","name":"Юг"}
205
215
  # {"$class":"Wallet","number":"W-10","Shop":{"$key":["Юг"]}}
206
216
  # {"$class":"Note","text":"x","$ext":"n1"} ← $ext — внешний ключ строки для ссылок { "$ext": "n1" }
@@ -264,6 +274,8 @@ const was = await db.Wallet(w).asOf('2026-10-01T00:00:00Z').first() // как
264
274
  await db.Shop(shop).Wallet().asOf(new Date('2026-10-01')).rows()
265
275
  const history = await db.Wallet(w).versions() // все версии; версия удаления — $deleted
266
276
  await db.Wallet(w200).versions({ follow: true }) // и через смену id (reclass, rekey)
277
+ await db.Wallet(w).diff() // что изменилось: предыдущая версия → последняя
278
+ await db.Wallet(w).blame() // кто, когда и почему (опция reason глагола) менял поля
267
279
  ```
268
280
  Класс с `history: false` не даёт `versions`, `asOf`, `restore` и событий подписки.
269
281
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,168 @@
2
2
 
3
3
  Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver.
4
4
 
5
+ ## [1.1.0] — 2026-10-11
6
+
7
+ letopis 1.1 по плану `reports/PLAN-LETOPIS-1.1.md` (редакция 5, этапы 0–3 и контрольная точка А): `diff` и `blame`
8
+ по журналу с причиной изменения, режимы сигналов, первая заливка; ревью безопасности №1. Совместимость с 1.0 не
9
+ сохраняется: процессам 1.0 и 1.1 не нужно работать с одной схемой; обновление схемы 1.0.1 (`up({ upgrade: true })`,
10
+ миграция `0006`) тестом не проверяется.
11
+
12
+ ### Changed — этап 0 (подготовка)
13
+ - Версия `1.1.0` (во время работы — `1.1.0-alpha.0`, не публиковалась).
14
+ - `db.describe()` и `describeText()` отдают условия класса по нескольким полям `anyOf` и `oneOf` (из
15
+ `resolved.schema`, с учётом наследования). Раньше агент, читавший описание, их не видел.
16
+
17
+ - Бенч против 1.0.1 (`bench/vs-101.bench.mjs`, dev-зависимость `letopis-1` = `npm:letopis@1.0.1`): засевка полигона
18
+ salon, загрузка без истории, построчный `create`, чтение по ключу, цепочка, `asOf`, двадцать писателей,
19
+ `maintain`. Опорный прогон на одинаковом коде (`bench/results/vs-101.md`) показывает шум замера: до ±21 % у
20
+ построчной записи и ±11 % у писателей — разницу меньше этого на точках А и Б считать нельзя.
21
+ - Карта: заготовки карточек модулей M-DERIVED и M-MCP (этапы 4 и 5 плана 1.1).
22
+
23
+ ### Fixed — этап 0
24
+ - Контракт API (`docs/api-contract.json`): поля `chain`, `db` и `batch` перечисляют методы цепочки, корня и батча,
25
+ а `removed` — снятое API с заменами. В 1.0 генератор писал `chain` пустым, а `removed` не находил ни одного
26
+ охранника: искал вызовы с тремя аргументами и только в `errors.ts`.
27
+
28
+ ### Tests — этап 0
29
+ - `telemetry`: условия `anyOf` и `oneOf` в `describe()`, наследование и замена своими условиями потомка, вывод
30
+ `describeText()`.
31
+
32
+ ### Added — этап 1 (`diff`, `blame`, причина изменения)
33
+ - Терминалы `diff({ from, to, follow })` и `blame({ at, limit })`: что изменилось между двумя версиями или моментами
34
+ и кто, когда и почему последним менял каждое поле. Пути — массивы с разделом строки в начале (`data`, `links`,
35
+ `tags`, `owner`, `class`); объекты данных — по ключам, массивы — целиком, множественные концы и теги — по
36
+ элементам. `exact: false` и `complete: false` — история неполная (прореживание, обрезка, невидимые версии,
37
+ предел `limit`, по умолчанию 1000 версий).
38
+ - Функции базы `jsonb_changes`, `version_changes`, `version_leaves`, `diff_versions`, `blame`
39
+ (`lib/sql/75-history.sql`) с правами вызывающего: сырой SQL получает то же под RLS журнала.
40
+ - Причина изменения из библиотеки: опция `reason` у `create`, `update`, `upsert`, `delete`, `anonymize`, `reclass`,
41
+ `restore`, `purge`, `rekey`; `db.begin({ reason })` и `db.batch(имя, { reason })` — для всех операций блока.
42
+ В 1.0 причину мог задать только сырой SQL настройкой `letopis.reason`.
43
+ - Зарезервированные имена `diff` и `blame`.
44
+ - Замер `bench/history.bench.mjs` (`docs/checkpoint-1.1.md`): `blame` по 1000 версиям — около 0,26–0,29 с, по
45
+ 10 000 — 1,5 с; `diff` — 4–12 мс.
46
+
47
+ ### Added — этап 1б (колонка изменённых путей, ревизия движка 6)
48
+ - Колонка журнала `changed`: пути изменения каждой версии против предыдущей. `blame` берёт их вместо сравнения
49
+ соседних версий; версии без путей (создание, загрузчик, импорт) досчитывает сравнением. Миграция
50
+ `lib/sql/migrate/0006-changed-paths.sql`; в снимок и хэш версии колонка не входит.
51
+ - `blame` и `diff_versions` — без JIT (`set jit = off`), соседние версии — окном `lead()`: `blame` по 1000 версиям —
52
+ 13,5 мс вместо 264, по 10 000 — 0,12 с вместо 1,5 (`docs/checkpoint-1.1.md`).
53
+
54
+ ### Tests — этап 1
55
+ - `history` (план 1.1, §7.2 №1–9; колонка `changed`: пути от триггера, `blame` одинаков с колонкой и с досчётом);
56
+ `roles` — новые функции приложения в белом списке.
57
+
58
+ ### Added — этап 2 (режимы сигналов)
59
+ - Режим сигналов установки `up({ signals })`: `class` (по умолчанию, как в 1.0) — имя класса, `tenant` — имя класса
60
+ и токен арендатора, `opaque` — только токены, `none` — сигналов нет. Токен — первые 96 бит HMAC-SHA256 на ключе
61
+ сигналов, выведенном из ключа строки `secret` с id System: без ключа псевдоним не сопоставить с именем, а
62
+ одинаковые сигналы транзакции PostgreSQL по-прежнему склеивает. Режим хранится в схеме (`meta` строки ключа):
63
+ повторный `up()` без опции его не меняет, `reset({ all })` сохраняет.
64
+ - Ключ сигналов — функция владельца `signal_key()` и право `letopis.signals` (системный сид выдаёт его сервисам:
65
+ ресурс `signals:API`, правило для `service:ACCOUNT`). Процесс с ключом сбрасывает кэш с точностью до арендатора
66
+ (строки System — во всех) и узнаёт классы по токенам; процесс без ключа сбрасывает класс во всех арендаторах, а в
67
+ режиме `opaque` — весь кэш на любой сигнал. Реестр, ключ и пропуска прав по `*`, незнакомым токенам, нагрузке не
68
+ своего режима (и у процесса без ключа в `opaque` — по любому сигналу) перечитываются не чаще раза в 5 с.
69
+ - Режим `none`: без LISTEN и без отправки сигналов, кэш только по сроку (`ttlOnly`), подписка опрашивает журнал,
70
+ реестр раз в 5 с сверяет отпечаток классов `class_stamp()`.
71
+ - Подписка просыпается только от своих классов и своего арендатора; без ключа — от любого сигнала, кроме сессий.
72
+ - `UpResult.signals`, `UpResult.warnings`, `InstallResult.signals`; `up()` предупреждает о PostgreSQL ниже 18.2
73
+ (сервер мог усечь CLOG, который ещё нужен непрочитанным сигналам), `checkServer()` возвращает предупреждения.
74
+ Тип `SignalMode`.
75
+ - Функции базы `signal(классы, арендаторы)` вместо `signal(классы)`, `signal_raw()`, `signal_token()`,
76
+ `signal_mode()`, `signal_key()`, `class_stamp()`; маркер установщика `@SIGNALS@`.
77
+ - Замер `bench/signals.bench.mjs` (`docs/checkpoint-1.1.md`): цены режима в скорости записи не видно на фоне шума;
78
+ `signal()` в режимах `tenant` и `opaque` дороже на 15 и 26 мкс на оператор; при записи одного арендатора ответы
79
+ другого остаются в кэше процесса с ключом (100 % попаданий против 0 % в режиме `class`).
80
+
81
+ ### Changed — этап 2
82
+ - Сигнал записи строится из пар «класс, арендатор»: триггеры, функции владельца, загрузчик и импорт (загрузчик и
83
+ импорт больше не зовут `pg_notify` сами); сессии и сброс — `signal_raw()`. `reset({ all })` шлёт `*` и
84
+ `session:*` без отдельного `Class`.
85
+ - После переподключения слушателя процесс перечитывает ключ и реестр, а не только сбрасывает кэш: сигнал `Class`,
86
+ пришедший во время обрыва, потерян. Перечитывания реестра по сигналу `Class` не идут одновременно: сигналы во
87
+ время перечитывания сливаются в одно следующее.
88
+ - Общий список классов прав — `ACL_CLASS_NAMES` в `types.ts` (был продублирован в `cache.ts` и `load.ts`).
89
+ - Бенчи `core-concurrency`, `core-write`, `core-notify`, `core-load` — на `signal_raw()` и новую `signal()`. Заодно
90
+ `core-concurrency` и `core-write` снова запускаются: их подмены `targets_bad` искали сигнатуру с двумя параметрами,
91
+ а замена `trg_entity_ai` в `core-write` была без `security definer` (сломались ещё до выпуска 1.0.0).
92
+
93
+ ### Fixed — этап 2
94
+ - Смена класса без ключа (сырой `update set class`) сигналит и прежний класс: в 1.0.1 сигнал шёл только по новому,
95
+ и ответы прежнего класса в кэше других процессов устаревали до срока.
96
+ - `Notifier` возвращает в очередь сигналы, которые не удалось отправить, и повторяет их следующим интервалом: в
97
+ 1.0.1 они терялись.
98
+
99
+ ### Tests — этап 2
100
+ - `signals` (план 1.1, §7.3 №1–9, №12–17): нагрузка у посторонней роли в каждом режиме, точность кэша до
101
+ арендатора, строки System, классы прав, процесс без ключа и тысяча записей, один путь у библиотеки и сырого SQL,
102
+ загрузчик, подписка, ключ, `none`, склейка, версия сервера, смена класса, повтор отправки, режим после `up()` и
103
+ `reset({ all })`. `engine-attacks` №31–32; `roles` — `signal_key` (негативный тест), `signal_mode`, `signal_key`,
104
+ `class_stamp` в белом списке; `system-classes` — 12 ресурсов и 8 правил, право сервиса `letopis.signals`;
105
+ `security-final` — формат нагрузки любого режима.
106
+
107
+ ### Added — этап 3 (первая заливка)
108
+ - `db.load(rows, { initial: true })`, `letopis load --initial`, `letopis import --initial`: загрузка в пустую схему
109
+ одной транзакцией — блокировка таблиц, проверка пустоты (кроме строк системных классов и записей `reset` —
110
+ `not_empty`), строки системных классов и определения ограничений, индексов и триггеров (с режимом включения) из
111
+ каталога, TRUNCATE, снятие индексов, ограничений и триггеров, `COPY … FREEZE`, перестройка, триггеры с прежним
112
+ режимом, проверки целей, владельцев и арендаторов по перестроенным индексам, сигнал, `ANALYZE`. Несовместима с
113
+ `commitEvery`, `existing` (кроме `'error'`) и `{ as }`; импорт с `--initial` — без `--update` и `--commit-every`,
114
+ строки системного сида пропускает. Код ошибки `not_empty`.
115
+ - Замер `bench/initial-load.mjs` (план 1.1, §7.4 №11; `docs/checkpoint-1.1.md`): 1,07 млн объектов полигона salon
116
+ первая заливка пишет за 113 с, обычный загрузчик — за 182 с (в 1,6 раза быстрее; цель плана — втрое), WAL — 1,4 ГБ
117
+ против 4,0 ГБ. Главный вклад — индексы GIN. `FREEZE` при загрузке ничего не стоит, а первое чтение и первый VACUUM
118
+ после неё не переписывают таблицу: около 12,5 с и 2,5 ГБ WAL экономии.
119
+
120
+ ### Changed — этап 3
121
+ - Загрузчик отправляет строки `COPY` кусками до 256 тысяч символов, а не отдельным сообщением `CopyData` на строку:
122
+ процессорное время загрузчика — вдвое меньше. Обычную загрузку ограничивает база, её скорость не изменилась; первая
123
+ заливка 52 тысяч объектов — 6,8 с вместо 8,3–9,2 с.
124
+
125
+ ### Fixed — этап 3
126
+ - `load({ verify: true })` в режиме истории ничего не проверял: список записанных строк оставался пустым.
127
+
128
+ ### Tests — этап 3
129
+ - `initial-load` (план 1.1, §7.4 №1–9): равенство с обычным загрузчиком (строки, журнал, хэши, `verify`), структура
130
+ и `ENABLE ALWAYS` после перестройки, `seq` и `tx` системных строк, `not_empty`, вставка во время запуска, откат,
131
+ `MULTI_INSERT` в WAL (`pg_walinspect`), `commitEvery`, импорт одной транзакцией.
132
+
133
+ ### Security — точка А (ревью безопасности №1)
134
+ - Колонка журнала `changed` приложению больше не выдана (право на `log` — по колонкам). Её пути описывают разницу
135
+ с предыдущей версией, и когда условие ACL скрывало версию, а следующую пропускало, в путях были видны снятые теги,
136
+ цели множественных концов и имена ключей скрытой версии. `blame` берёт пути функцией владельца `version_paths` —
137
+ только когда видны обе версии пары. `select * from log` от приложения теперь отказ: колонки перечисляются явно;
138
+ `version_changes(log, log)` и `version_leaves(log)` заменены функциями по колонкам `row_changes` и `row_leaves`.
139
+ - Перечитывание реестра по сигналу `Class`: три подряд — сразу, дальше не чаще раза в 5 с — заливка канала
140
+ поддельными `Class` держала процессы в непрерывном перечитывании. Счётчиков сигналов в кэше — не больше 100 тысяч в
141
+ каждой карте: заливка разными поддельными именами копила их без предела.
142
+
143
+ ### Changed — точка А
144
+ - Первая заливка — конвейером: следующая пачка готовится, пока база пишет текущую (та же транзакция, `FREEZE`).
145
+ На 1,07 млн объектов — 77 с вместо 113 с, в 2,2 раза быстрее обычного загрузчика (169 с); обычный загрузчик
146
+ по-прежнему пишет пачки по очереди (`docs/checkpoint-1.1.md`).
147
+
148
+ ### Fixed — точка А
149
+ - `blame` после пропуска версии (прореживание, обрезка, невидимая версия) приписывал лист, изменённый выпавшей
150
+ версией, более старой версии с `exact: true`: пути колонки `changed` берутся только у пары без пропуска, а `exact`
151
+ учитывает пропуски от текущей версии до найденной.
152
+ - Пути изменения в триггере правки: множественный конец, который не менялся, больше не раскладывается на элементы,
153
+ а изменившийся считается разностью множеств — правка любого поля объекта с тысячами целей стоила секунды.
154
+ - Причина `db.batch(имя, { reason })` записывается в глагол при постановке в очередь: `run()`, вызванный с другого
155
+ фасада той же очереди, её терял.
156
+
157
+ ### Tests — точка А
158
+ - `history`: версия, скрытая условием ACL, — колонка закрыта, в `blame` и `diff` её нет; лист, изменённый выпавшей
159
+ версией; `row_changes` на 20 тысячах целей; причина батча с другого фасада. `roles` — права на колонки журнала и
160
+ `version_paths`; `cache` №17 — предел счётчиков; `engine-attacks` №32 — поддельные `Class`.
161
+ - Гонки в тестах, ронявшие CI (библиотека не менялась): подписка `'now'` пишет только после начальной точки
162
+ (`watchStarted` — `watch`, `retention`, сценарий салона), `take` после таймаута не зовёт `next()` второй раз; прокси
163
+ обрыва `'after'` рвёт связь, когда база уже исполнила COMMIT, а подключение через него — без продления сессии
164
+ (`balance` №13–14, №25–27); С15 (`scenario-real-life`) — оба порядка отмены и переноса через ожидание блокировки;
165
+ 5.2 (`auth`) — замеры парами вперемешку.
166
+
5
167
  ## [1.0.1] — 2026-10-09
6
168
 
7
169
  Исправления, найденные сразу после выпуска 1.0.0.
package/README.en.md CHANGED
@@ -1,4 +1,4 @@
1
- # letopis 1.0
1
+ # letopis 1.1
2
2
 
3
3
  **letopis** is a Node.js library: a store of versioned entities on PostgreSQL 18. Every change is saved as a version
4
4
  in a journal. So every object has a history, a state as of any date and a change subscription. The database itself
@@ -191,10 +191,23 @@ is built from the journal.
191
191
 
192
192
  ### Signals
193
193
 
194
- A write reports the changed classes through `NOTIFY`. Raw SQL sends the signal from a trigger, and PostgreSQL delivers
195
- it on commit. Library transactions defer their signals: the process notifier sends them every 100 ms
196
- (`notifyIntervalMs`). A signal carries only a class name or the start of the token hash of a revoked session: no ids,
197
- no data. On signals, the class registry is re-read, the result cache is cleared and subscriptions wake up.
194
+ A write reports changes through `NOTIFY` on the schema channel. Raw SQL sends the signal from a trigger, and PostgreSQL
195
+ delivers it on commit. Library transactions defer their signals: the process notifier sends them every 100 ms
196
+ (`notifyIntervalMs`) and retries the ones that failed to send. What a write signal carries is set by the installation
197
+ mode `up({ signals })`:
198
+
199
+ | Mode | Write signal | What any role that can connect to the database sees |
200
+ |---|---|---|
201
+ | `class` (default) | the class name: `Booking` | class names and write times |
202
+ | `tenant` | the class name and a tenant token: `Booking@3f9c…` | also tenant pseudonyms |
203
+ | `opaque` | a class token and a tenant token | only write times and frequency per pseudonym |
204
+ | `none` | no signals | nothing |
205
+
206
+ A revoked session sends `session:<start of the token hash>`, a reset sends `*`; signals carry no ids and no data. A
207
+ token is a keyed HMAC: only a process with the signal key can match a pseudonym to a name, and the key is given to
208
+ services (the `letopis.signals` permission). On signals, the class registry is re-read, the result cache is cleared
209
+ and subscriptions wake up; what the mode changes for the cache and subscriptions is in
210
+ [section 12](#12-change-subscription-and-cache).
198
211
 
199
212
  ## 3. Installation and connection
200
213
 
@@ -213,6 +226,7 @@ functions and the system seed. On the first installation, it issues a service ke
213
226
  | `history` | the history retention policy for classes without their own ([section 11](#11-history-and-point-in-time-reads)) |
214
227
  | `serviceKey` | your own service key instead of a random one: `lts_` and 32–128 hexadecimal characters |
215
228
  | `allowReset` | allow `db.reset()` ([section 13](#13-operations)); off by default |
229
+ | `signals` | the signal mode: `'class'` (default), `'tenant'`, `'opaque'` or `'none'` (["Signals"](#signals)); the mode is stored in the schema — `up()` without the option keeps it |
216
230
  | `fresh` | drop the schema and install it again — **the schema data is lost** |
217
231
  | `upgrade` | upgrade a schema of an older revision: migrations and a re-apply |
218
232
  | `pglite` | a database for development without PostgreSQL (see below) |
@@ -221,7 +235,17 @@ functions and the system seed. On the first installation, it issues a service ke
221
235
 
222
236
  Result: `schema` (the full name), `serviceKey` (on the first installation, and on a re-run when the installation has no
223
237
  valid API key left; otherwise `null`), `created`, `upgraded`, `revision`, `migrations`, `analyzed`, `owner`, `app`,
224
- and also `tenant` and `pglite` if they were requested.
238
+ `signals` (the signal mode of the installation), `warnings` (without `quiet` they are printed), and also `tenant` and
239
+ `pglite` if they were requested. On PostgreSQL below 18.2, `up()` warns: such a server could truncate CLOG that unread
240
+ signals still need — upgrade it.
241
+
242
+ To hide class names from signals, use the `opaque` mode (or `tenant` if the names are not secret but you want the
243
+ cache to be precise per tenant); after changing the mode, restart the application processes — they read the mode on
244
+ connect:
245
+
246
+ ```ts
247
+ await up({ dsn: ADMIN, schema: 'shop', signals: 'opaque' }) // later up({ dsn, schema }) keeps the mode
248
+ ```
225
249
 
226
250
  <!-- run: guide -->
227
251
  ```ts
@@ -1077,6 +1101,23 @@ regular write: the tenant is available to the user, write permission on each cla
1077
1101
  the visibility of reference targets, the owner per `ownerDefault`. Without `tenant`, the rows go to the user's own
1078
1102
  tenant. This is how a loading process can accept files sent by users.
1079
1103
 
1104
+ ### Initial load
1105
+
1106
+ ```ts
1107
+ const r = await adm.load(rows, { initial: true }) // empty schema: one transaction, indexes and triggers at the end
1108
+ ```
1109
+
1110
+ `db.load(rows, { initial: true })` (`letopis load --initial`) loads data into an empty schema faster than the regular
1111
+ loader — about 2.2 times on a million objects: everything goes in one transaction, the tables are closed for reading and
1112
+ writing during the load, indexes, constraints and triggers are dropped and rebuilt at the end, and rows are written with
1113
+ `COPY … FREEZE`. Empty means that the schema has nothing besides the rows of system classes (class descriptions,
1114
+ accounts, memberships, resources, rules) and `reset` records; otherwise you get `not_empty` before any change. The mode
1115
+ is incompatible with `commitEvery`, `existing` (other than `'error'`) and `{ as }`; an error in any row rolls
1116
+ everything back, indexes and triggers included. `letopis import --initial` runs the whole import this way: in one
1117
+ transaction, without `--update` and `--commit-every`. WAL is written here too (it is skipped only with
1118
+ `wal_level = minimal`), but the indexes are built once, so there is almost three times less of it; after `FREEZE`, the
1119
+ first read and the first vacuum do not rewrite the table.
1120
+
1080
1121
  ### Command line
1081
1122
 
1082
1123
  ```sh
@@ -1324,8 +1365,10 @@ const followed = await db.Product(mug2).versions({ follow: true }) // also acros
1324
1365
  ```
1325
1366
 
1326
1367
  Every version has an author, an agent, a time, a number, a kind of operation (`create`, `update`, `delete`, `reclass`,
1327
- `rekey`, `restore`, `purge`, `trim`, `reset`) and a reason (`reason`), if the `letopis.reason` setting of the
1328
- transaction set one; there is no separate API for the reason. A deletion version has `$deleted`.
1368
+ `rekey`, `restore`, `purge`, `trim`, `reset`) and a reason (`reason`). The reason comes from the `reason` option of a verb
1369
+ (`update(patch, { reason })`, `create(data, { reason })`, `delete({ confirm, reason })` and others); for every
1370
+ operation of a block, from `db.begin({ reason })` and `db.batch(name, { reason })`; in raw SQL, from the
1371
+ `letopis.reason` setting of the transaction. A deletion version has `$deleted`.
1329
1372
 
1330
1373
  ### Point-in-time read
1331
1374
 
@@ -1371,6 +1414,34 @@ period boundaries are in the `tz` time zone (UTC by default).
1371
1414
  - In the thinned part, a point-in-time read shows the last kept version before the moment, so its precision equals the
1372
1415
  period of the tier. A subscriber that lags behind by more than the `all` tier gets `cursor_expired`.
1373
1416
 
1417
+ ### What changed and who: `diff` and `blame`
1418
+
1419
+ ```ts
1420
+ const [d] = await db.Order(id).diff() // previous version → latest
1421
+ await db.Order(id).diff({ from: 3, to: 7 }) // by version numbers
1422
+ await db.Order(id).diff({ from: '2026-10-01' }) // state at a moment → latest
1423
+ await db.Order(id).diff({ follow: true }) // across id changes (reclass, rekey)
1424
+ const [b] = await db.Order(id).blame() // every field of the latest version
1425
+ await db.Order(id).blame({ at: '2026-10-01', limit: 200 }) // the version at a moment; scan 200 versions
1426
+ ```
1427
+
1428
+ - `diff()` returns one item per object of the step: `{ id, class, from, to, changes }`, where a change is
1429
+ `{ path, kind, before, after }`, `kind` is `added`, `removed` or `changed`, and the path starts with the section of
1430
+ the row: `data`, `links`, `tags`, `owner`, `class`. Data objects are compared by keys, arrays as a whole; a
1431
+ multi-valued end and tags element by element. If the object did not exist at `from`, every path is `added`; if it
1432
+ was deleted at `to`, `to.deleted` is `true`.
1433
+ - `blame()` returns `{ id, class, rev, complete, fields }`: for every leaf, the version that changed it last,
1434
+ `author`, `agent`, `op`, `reason` and `exact`. `exact: false` means there are gaps between the current version and
1435
+ that one (the history is thinned, trimmed or partly invisible): a missing version may have changed the leaf too.
1436
+ `complete: true` means the whole history was scanned without gaps; by default at most the 1000 latest versions are
1437
+ scanned.
1438
+ - The database computes both with the caller's rights: raw SQL gets the same, `select "v2.shop".blame('<id>')`,
1439
+ `select "v2.shop".diff_versions('<id>')`.
1440
+ - The journal column `changed` (the paths changed by a version, which make `blame` fast) is not granted to the
1441
+ application: the paths describe the difference from the previous version, and the reader may not see that one.
1442
+ So `select * from log` does not work for the application — list the columns; `blame` gets the paths through a
1443
+ database function only when both versions are visible.
1444
+
1374
1445
  ### Trimming history
1375
1446
 
1376
1447
  <!-- run: guide -->
@@ -1439,11 +1510,21 @@ stays correct:
1439
1510
  - the result key includes the query, the parameters, the session, the actor and the tenant, so one user never gets the
1440
1511
  result of another user;
1441
1512
  - your own writes clear the results of the changed classes right after commit (including the cascade classes); other
1442
- writers' changes clear them on a database signal;
1513
+ writers' changes clear them on a database signal. In the `tenant` and `opaque` modes, a process with the signal key
1514
+ clears a class only in the tenant of the write (System rows — in all tenants); a process without the key clears it
1515
+ in all tenants, and in the `opaque` mode any signal clears its whole cache;
1443
1516
  - changes of permissions and classes, writes through `db.sql`, a revoked session and a listener reconnect clear the
1444
1517
  whole cache or the results of the session;
1445
1518
  - the default term is 60 seconds; the cache does not work inside `db.begin()`;
1446
- - without `LISTEN` the cache is off, and `connect({ cache: { ttlOnly: true } })` enables it by term only.
1519
+ - without `LISTEN` and in the `none` mode the cache is off, and `connect({ cache: { ttlOnly: true } })` enables it by
1520
+ term only.
1521
+
1522
+ A process re-reads the class registry right away on a class change signal (one re-read at a time), and the registry
1523
+ and the signal key on `*`, on an unknown token and on a payload of another mode (and without the key in the `opaque`
1524
+ mode — on any signal) not more often than once in 5 seconds: a stream of signals, fake ones included, does not turn
1525
+ into a stream of queries. In the `none` mode, the registry checks the class
1526
+ fingerprint every 5 seconds, and subscriptions poll the log. A subscription wakes up only for its own classes and its
1527
+ own tenant, and without the key — for any signal; it always reads the events from the log, so nothing is lost.
1447
1528
 
1448
1529
  Between processes, the cache goes stale for the signal sending interval (`notifyIntervalMs`, 100 ms) plus delivery;
1449
1530
  `db.flushSignals()` sends the accumulated signals at once. `db.cache.clear()` clears the cache.
@@ -1490,7 +1571,8 @@ const modelText = describeText(model) // the same as text: for the application's
1490
1571
  ```
1491
1572
 
1492
1573
  `db.describe()` returns a description of the classes available to the session: fields, ends, keys, policies and
1493
- metadata. From the command line: `letopis describe --format text --out llms.txt`.
1574
+ metadata, plus the multi-field conditions `anyOf` and `oneOf` with inheritance applied, when a class has them. From
1575
+ the command line: `letopis describe --format text --out llms.txt`.
1494
1576
 
1495
1577
  ### Raw SQL
1496
1578
 
@@ -1511,12 +1593,12 @@ application.
1511
1593
  letopis sync <module> --dsn <dsn> --schema <v2.name> (--token <token> | --api-key <key>) [--apply]
1512
1594
  letopis types --dsn <dsn> --schema <v2.name> (--token <token> | --api-key <key>) [--out <file>]
1513
1595
  letopis load <files…> --dsn <dsn> --schema <v2.name> (--token <token> | --api-key <key>) [--class <Class>]
1514
- [--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--verify]
1596
+ [--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--verify] [--initial]
1515
1597
  letopis verify --dsn <dsn> --schema <v2.name> [--token <token> | --api-key <key>] [--data]
1516
1598
  letopis maintain --dsn <dsn> --schema <v2.name> [--token <token> | --api-key <key>] [--batches <N>]
1517
1599
  letopis describe --dsn <dsn> --schema <v2.name> (--token <token> | --api-key <key>) [--format json|text] [--out <file>]
1518
1600
  letopis import --from <0.21 dsn> --from-schema <v1.name> --dsn <dsn> --schema <v2.name> [--tenant <name>] [--keyless <Class>]…
1519
- [--rename <Old>=<New>]… [--update] [--commit-every <N>] [--partition <name>]
1601
+ [--rename <Old>=<New>]… [--update] [--commit-every <N>] [--partition <name>] [--initial]
1520
1602
  ```
1521
1603
 
1522
1604
  ### Monitoring
@@ -1669,7 +1751,7 @@ database does not accept a link with such a name (`reserved_names()`; the instal
1669
1751
  - chain methods: `then`, `entity`, `paths`, `rows`, `first`, `ids`, `count`, `limit`, `offset`, `sort`, `asOf`,
1670
1752
  `withDeleted`, `deep`, `exact`, `sum`, `avg`, `min`, `max`, `countBy`, `after`, `versions`, `create`, `update`,
1671
1753
  `upsert`, `set`, `unset`, `add`, `remove`, `delete`, `anonymize`, `reclass`, `restore`, `purge`, `alias`, `tags`,
1672
- `owner`, `forUpdate`, `cache`, `rekey`; guards of the removed API: `run`, `execute`, `account`;
1754
+ `owner`, `forUpdate`, `cache`, `rekey`, `diff`, `blame`; guards of the removed API: `run`, `execute`, `account`;
1673
1755
  - the `db` root: `as`, `begin`, `commit`, `rollback`, `lock`, `batch`, `watch`, `close`, `registry`, `sql`, `load`,
1674
1756
  `verify`, `maintain`, `reset`, `describe`, `auth`, `acl`, `idOf`, `refresh`, `transaction`, `flushSignals`,
1675
1757
  `schema`, `token`, `tenant`, `trimHistory`, `rekeyClass`, `cache`; guards: `reloadSchema`, `accounts`,
@@ -1720,6 +1802,7 @@ Library codes:
1720
1802
  | `no_path` | there is no hop between the classes of neighboring steps |
1721
1803
  | `invalid_query` | the chain or the API is used incorrectly (a step modifier `deep`, `exact`, `alias`, `tags` or `owner` after a verb, a slot not after a verb, an `after()` cursor with a value of a different type than the `sort` field, without `v` or with an `id` that is not a uuid, and so on) |
1722
1804
  | `admin_required` | the loader needs an admin connection |
1805
+ | `not_empty` | the initial load (`initial`) goes only into an empty schema |
1723
1806
  | `class_not_loadable` | the loader does not accept `Class` rows |
1724
1807
  | `bypass_rls` | `connect()` with a role that bypasses RLS, without `allowBypassRls` |
1725
1808
  | `removed` | a removed 0.21 API; the text gives the replacement |
@@ -1758,9 +1841,15 @@ database from every write (row-by-row writes: 119 → 128–147 rows/s in differ
1758
1841
  12–18% slower than 0.21 (the measurements vary): most of the time goes to the check triggers in the database and to the
1759
1842
  commit with a disk write.
1760
1843
 
1844
+ Initial load (`cd lib && npx tsx bench/initial-load.mjs`, 2026-10-11, median of three rounds): the regular loader
1845
+ writes 1.07 million objects of the salon dataset without history in 169 s (6,351 rows/s, 4.0 GB of WAL), the initial
1846
+ load in 77 s (13,902 rows/s, 1.4 GB of WAL). Most of the difference comes from the GIN indexes: maintaining them during
1847
+ the load costs 36 s, building them at the end about 5 s. Preparing rows in the loader itself (checks, ends, ids) runs
1848
+ alongside the `COPY` of the previous batch and now sets the pace: about 60 s of the 77.
1849
+
1761
1850
  ## 18. Limitations
1762
1851
 
1763
- This section lists what letopis 1.0 deliberately does not do or does with caveats, and where you need to watch the
1852
+ This section lists what letopis 1.1 deliberately does not do or does with caveats, and where you need to watch the
1764
1853
  behavior.
1765
1854
 
1766
1855
  **Platform**
@@ -1777,6 +1866,9 @@ behavior.
1777
1866
  - The NUL character (`\u0000`) is impossible in data strings: PostgreSQL does not store it in `jsonb`; such data is
1778
1867
  rejected already when the JSON is parsed (in the official JSON-Schema-Test-Suite, the "nul characters" groups are
1779
1868
  skipped).
1869
+ - On PostgreSQL before 18.6, a parallel GIN index build could write a wrong row count into the table statistics
1870
+ (`pg_class.reltuples`), after which autovacuum stopped processing the table. That is why the initial load always ends
1871
+ with `ANALYZE`, but it is better to upgrade the server to 18.6 or newer.
1780
1872
 
1781
1873
  **Writing and integrity**
1782
1874
 
@@ -1786,6 +1878,10 @@ behavior.
1786
1878
  transactions always open at this level.
1787
1879
  - PostgreSQL does not run `COPY` into tables under RLS; bulk loading works only through the loader on an admin
1788
1880
  connection.
1881
+ - The initial load closes the schema's tables for the whole load, for writing and for reading: it is meant for an
1882
+ empty schema (seeding, migration, import), not for a running system. Its gain is in the database (indexes and
1883
+ constraints are built once); it does not speed up the row preparation in the loader, only runs it alongside `COPY`,
1884
+ so on a million objects it is about 2.2 times faster than the regular load.
1789
1885
  - The database trusts the loader: the data, ends and ids of loaded rows are checked by the library code (the TypeScript
1790
1886
  validator), not by the triggers; the database checks only the targets outside the load, the tenants and the owners,
1791
1887
  with one query at the end. The safeguards: the loader is available only to an admin, differential tests of the
@@ -1858,16 +1954,26 @@ behavior.
1858
1954
  - The journal has no partitions: thinning deletes versions in batches, and the table file does not shrink by itself
1859
1955
  (new versions take the space after autovacuum); to return the space to the system, use `VACUUM FULL` or `pg_repack`
1860
1956
  in a maintenance window.
1861
- - Signals reveal activity: PostgreSQL lets any role listen to a channel, and the class names in signals show when and
1862
- to which classes writes happen (a signal has no ids, no tenant and no data). A fake signal only clears the cache one
1863
- extra time.
1957
+ - Signals reveal activity: PostgreSQL lets any role listen to a channel. In the `class` mode one sees when and to which
1958
+ classes writes happen; `tenant` adds tenant pseudonyms; `opaque` hides what was written but not when: the write
1959
+ time and frequency per pseudonym stay visible, and a deterministic token is the basis of frequency attacks. Write
1960
+ times are visible without signals too: `pg_stat_all_tables` and `pg_locks` are open to PUBLIC. The `none` mode
1961
+ removes signals, but then a revoked session leaves the cache of other processes only by term, and a new class
1962
+ appears in their registry within 5 seconds. Real protection from foreign roles is a separate database. A fake
1963
+ signal only clears the cache one extra time: on `*` and unknown tokens the registry is re-read not more often than
1964
+ once in 5 seconds, and a fake class change signal re-reads it like a real one, but with at most one re-read at a
1965
+ time.
1966
+ - The signal mode is changed by `up({ signals })`, and the processes have to be restarted after that: a process reads
1967
+ the mode and the signal key on connect (it treats a payload of another mode as foreign and clears the whole cache).
1968
+ - PostgreSQL below 18.2 could truncate CLOG that unread signals still need: `up()` warns about it.
1864
1969
  - The `NOTIFY` queue (8 GB) is shared by the whole cluster: a stuck listener fills it up, and then transactions with
1865
1970
  `NOTIFY` fail on commit. The library reads signals right away, and `maintain()` warns when the queue is more than
1866
1971
  half full.
1867
1972
  - The subscription horizon is shared by the whole cluster: a long write transaction delays the events of all
1868
1973
  subscribers (`sub.lag()`); long operations are better done in batches.
1869
1974
  - Between processes, the result cache goes stale for the signal sending interval (100 ms) plus delivery, and for the
1870
- time of a listener reconnect; the `ttl` term is a safeguard.
1975
+ time of a listener reconnect: postgres.js does not report a broken listener connection, so the cache works until the
1976
+ reconnect and is cleared completely after it. The `ttl` term is a safeguard.
1871
1977
  - A "from now" subscription cursor carries a snapshot of the subscription moment until it passes that boundary: such a
1872
1978
  cursor is longer than a regular one.
1873
1979
 
@@ -1898,7 +2004,7 @@ This section is for those who develop letopis itself. Run: `cd lib && npm test`.
1898
2004
  # chain-write — writing with chains: create, update, upsert, delete with preview, anonymize, reclass, slots, inc, { rev }, fan-out, plan rollback (stage 4)
1899
2005
  # demo — the booking demo domain on Zod: seed from models, writing data, id by key, agreement between Zod and the database (stage 2)
1900
2006
  # docs — documentation examples are executed: the marked blocks of the README and the cheatsheet (Russian and English) run as scenarios against PostgreSQL 18 (stage 8)
1901
- # engine-attacks — raw SQL attacks from the application role: the rows of §7.3.1 for stages 1 and 3 with specific outcomes (stages 1, 3)
2007
+ # engine-attacks — raw SQL attacks from the application role: the rows of §7.3.1 for stages 1 and 3 with specific outcomes; the signal key without permission, flooding with fake signals, Class included (stages 1, 3; plan 1.1, stage 2 and checkpoint A)
1902
2008
  # engine-class — classes as entities: metaschema, inheritance, recomputing descendants, tightening, metadata (stage 1)
1903
2009
  # engine-write — the write pipeline in raw SQL: v5 ids, defaults, merge, inc, deletion, resurrection, journal, signals (stage 1)
1904
2010
  # engine-races — races on two connections in both orders, without timed pauses (stage 1)
@@ -1912,7 +2018,7 @@ This section is for those who develop letopis itself. Run: `cd lib && npm test`.
1912
2018
  # member — memberships: working in a tenant by roles, auth.switch, leaving with ownership transfer, system rows, tenant roles without System permissions (stage 5)
1913
2019
  # model — Zod models, the JSON Schema normalizer, pattern translation, agreement with the database metaschema (stage 2)
1914
2020
  # load — the loader: several classes, cycles, $key and $ext, repeats, existing, portions, history mode, the query for targets outside the load, CLI (stage 4)
1915
- # roles — roles and privileges: installation without a superuser, owner functions, triggers by catalog (stage 1)
2021
+ # roles — roles and privileges: installation without a superuser, owner functions, triggers by catalog, the log without the changed column (stage 1; plan 1.1, checkpoint A)
1916
2022
  # write-races — write races: 20 concurrent edits of different fields without losses and strict rev, concurrent add/remove on a multiple end, the loader versus class tightening and versus a concurrent edit (existing: update) in both orders (stage 4)
1917
2023
  # validator-parity — the TypeScript validator versus the database check: the official suite, 10,000 random documents, defaults (stage 2)
1918
2024
  # scenario-barbershop — end-to-end barbershop scenario: 9 scenes from 0.21 (integration) on 1.0 chains — catalog, free slots in a batch, bookings under lock, multi-slot and multi-specialist, versions, cancellation and booking again on the same id, closing a month with restrict (stage 4)
@@ -1928,11 +2034,14 @@ This section is for those who develop letopis itself. Run: `cd lib && npm test`.
1928
2034
  # verify — legitimate gaps and tampering, verify({ data: true }): data, ends, tenants, id by key; permissions and CLI (stage 6)
1929
2035
  # 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)
1930
2036
  # watch — cursor subscription: commit order, a long transaction, resuming, listener drop, two tenants (stage 6)
1931
- # 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 (stage 6)
2037
+ # 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)
1932
2038
  # maintain — maintenance: expired sessions and codes, permissions, one process under a lock, schedule (stage 6)
1933
2039
  # 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)
1934
2040
  # scenario-salon — end-to-end salon scenario: 21 acts from 0.21 (salon) with a coverage matrix of the 1.0 API (stage 6)
1935
2041
  # telemetry — onQuery and slowMs, describe() and class metadata with inheritance, describeText (stage 6)
2042
+ # 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)
2043
+ # signals — signal modes: the payload seen by a foreign role, cache precision per tenant, System, permission classes, a process without the key, the loader, subscriptions, the key, none, coalescing, the server version, a class change, a resend, the mode after up() and reset({ all }) (plan 1.1, stage 2)
2044
+ # initial-load — the initial load: equality with the regular loader, structure and trigger modes, system rows, a non-empty schema, an insert during the start, rollback, MULTI_INSERT in WAL, commitEvery, import --initial (plan 1.1, stage 3)
1936
2045
  # 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)
1937
- # 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; the cache key distinguishes the token, the actor and the tenant (stage 8)
2046
+ # 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)
1938
2047
  ```