letopis 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/AGENT-CHEATSHEET.md +484 -414
  2. package/CHANGELOG.md +321 -0
  3. package/LICENSE +75 -21
  4. package/LICENSE-COMMERCIAL.md +49 -0
  5. package/README.md +786 -183
  6. package/api-contract.json +381 -0
  7. package/dist/acl.d.ts +3 -0
  8. package/dist/acl.js +19 -2
  9. package/dist/admin.d.ts +2 -1
  10. package/dist/admin.js +6 -4
  11. package/dist/auth.d.ts +18 -3
  12. package/dist/auth.js +75 -6
  13. package/dist/chain.d.ts +38 -17
  14. package/dist/chain.js +434 -63
  15. package/dist/cli.js +28 -6
  16. package/dist/errors.d.ts +19 -1
  17. package/dist/errors.js +46 -6
  18. package/dist/index.d.ts +10 -4
  19. package/dist/index.js +136 -46
  20. package/dist/internal.d.ts +2 -0
  21. package/dist/load.d.ts +9 -5
  22. package/dist/load.js +30 -11
  23. package/dist/log.d.ts +121 -0
  24. package/dist/log.js +633 -0
  25. package/dist/mcp.js +26 -11
  26. package/dist/messages/api.d.ts +66 -0
  27. package/dist/messages/api.js +90 -1
  28. package/dist/messages/boot.js +4 -4
  29. package/dist/messages/codes.js +4 -4
  30. package/dist/messages/index.d.ts +5 -2
  31. package/dist/messages/index.js +5 -2
  32. package/dist/messages/log.d.ts +229 -0
  33. package/dist/messages/log.js +102 -0
  34. package/dist/messages/model.d.ts +24 -1
  35. package/dist/messages/model.js +34 -3
  36. package/dist/messages/query.d.ts +10 -0
  37. package/dist/messages/query.js +13 -0
  38. package/dist/messages/sql-core.d.ts +15 -1
  39. package/dist/messages/sql-core.js +20 -3
  40. package/dist/messages/sql-time.d.ts +15 -0
  41. package/dist/messages/sql-time.js +23 -0
  42. package/dist/messages/write.d.ts +47 -0
  43. package/dist/messages/write.js +63 -0
  44. package/dist/model.d.ts +4 -0
  45. package/dist/model.js +23 -5
  46. package/dist/sql.d.ts +2 -0
  47. package/dist/sql.js +77 -25
  48. package/dist/sync.d.ts +1 -1
  49. package/dist/sync.js +50 -0
  50. package/dist/tx.d.ts +10 -1
  51. package/dist/tx.js +44 -2
  52. package/dist/typed.d.ts +14 -10
  53. package/dist/types.d.ts +82 -4
  54. package/dist/types.js +6 -1
  55. package/dist/up.d.ts +3 -3
  56. package/dist/up.js +57 -17
  57. package/dist/uuid.d.ts +8 -0
  58. package/dist/uuid.js +9 -1
  59. package/dist/validate.js +24 -7
  60. package/dist/watch.d.ts +5 -0
  61. package/dist/watch.js +13 -0
  62. package/dist/write.d.ts +78 -1
  63. package/dist/write.js +357 -39
  64. package/llms.txt +37 -17
  65. package/package.json +12 -6
  66. package/sql/20-context.sql +11 -3
  67. package/sql/30-validate.sql +51 -10
  68. package/sql/40-class.sql +21 -1
  69. package/sql/80-auth.sql +88 -17
  70. package/sql/90-time.sql +34 -6
  71. package/sql/99-revision.sql +5 -1
  72. package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
  73. package/sql/migrate/0010-impersonation-no-account.sql +5 -0
  74. package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
  75. package/sql/migrate/README.md +5 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,327 @@
4
4
  «Версии и совместимость» в README; шаг версии сверяет `npm run check:semver`. С 1.4.2 у правки ядра (библиотеки и SQL)
5
5
  в конце пункта указан источник находки: «Источник: тест», «пример», «сервис», «ревью», «soak» или «внедрение».
6
6
 
7
+ ## [3.0.0] — 2026-11-03
8
+
9
+ Мажор: безопасность по умолчанию и журнал событий. Удаление и восстановление без сверки с тем, что видел пользователь,
10
+ теперь отказ, а не молчаливое действие; `createTenant` проверяет свой первый аргумент; вместо голых `TypeError` и
11
+ вводящих в заблуждение отказов — ошибки letopis с подсказкой. Ревизия движка 9 → 11: схему 2.x обновляет
12
+ `up({ schema, upgrade: true })` (миграции `sql/migrate/0010-impersonation-no-account.sql` — новое тело `ctx()`, и
13
+ `sql/migrate/0011-key-lock.otp-enabled.sql` — новые тела функций входа по ключу, отзыва креда, OTP и TOTP; раздел
14
+ «Исправления»).
15
+
16
+ ### Переход на 3.0
17
+ Что ломается и как переписать:
18
+
19
+ - **`delete({ confirm: true })` без `expect`** — отказ `invalid_query`, ключ `write.confirm_without_expect`, ничего не
20
+ удалено. Удаляйте ровно показанное превью:
21
+
22
+ ```ts
23
+ const preview = await db.Item(id).delete().rows(); // показать пользователю
24
+ await db.Item(id).delete({ confirm: true, expect: preview }).rows();
25
+ ```
26
+
27
+ Где удаление без превью — осознанный замысел (служебная чистка, удаление, подтверждённое в окне без показа
28
+ каскада), — `delete({ confirm: true, all: true })`: удалить текущий состав целей без сверки (без фильтра — все
29
+ строки класса, как прежде `all: true`). Правило одно для цепочек, `db.begin()`, `db.batch()` и `db.as()`.
30
+ - **`restore()` без `expect`** — отказ `invalid_query`, ключ `write.restore_without_expect`, ничего не восстановлено, и
31
+ для явных id тоже (`db.Item(id).restore()`): между удалением и восстановлением объект могли вернуть, изменить и удалить
32
+ снова — `restore()` вернул бы надгробие, которого пользователь не видел. Передавайте надгробия:
33
+
34
+ ```ts
35
+ const gone = await db.Item(id).delete({ confirm: true, expect: preview }).rows();
36
+ await db.Item(id).restore({ expect: gone }).rows();
37
+ // или по корзине: const tomb = await db.Item(id).withDeleted().rows(); restore({ expect: tomb })
38
+ ```
39
+
40
+ Вернуть то, что можно, — `restore({ expect, partial: true })`; осознанно вернуть все удалённые цели шага без сверки —
41
+ `restore({ all: true })`. Предупреждение `write.restore_no_filter` снято: без `expect` и `all` теперь отказ.
42
+ - **`createTenant(admin, …)`** — `admin` обязан быть пулом `postgres()` (функция с `begin` и `unsafe`); строка DSN,
43
+ подключение `connect()` letopis, `undefined` или другой объект — отказ `invalid_query`, ключ
44
+ `api.create_tenant_admin` (раньше — `TypeError: sql.begin is not a function`). Имя схемы нормализуется как в
45
+ `connect()` и `up()`: `createTenant(admin, 'salon', …)` работает со схемой `v2.salon` (раньше — сырая ошибка
46
+ PostgreSQL `42P01`).
47
+ - Типы опций: `delete(opts?: DeleteOptions)` и `restore(opts: RestoreOptions)` (новые экспортируемые типы) —
48
+ `delete({ confirm: true })` без `expect` и `all`, а также `restore()` без аргумента TypeScript отклоняет при
49
+ компиляции. `confirm` — литерал `true`, а не `boolean`.
50
+ - `svc.as(id)` с id аккаунта, которого нет (не заводили или удалён), — `target_not_found`, ключ
51
+ `sql-core.impersonation_no_account` «аккаунт … не найден» (раньше — `acl_denied` «действовать от имени этого аккаунта
52
+ нельзя», как запрет прав). Запрет (System, сервис, системный администратор, выключенный аккаунт) — как прежде
53
+ `acl_denied`.
54
+ - Связь, переданная полем данных (`create({ …, Member: id })`), — `invalid_data` с новым ключом `write.link_in_data`
55
+ вместо `api.validation_failed_end`: текст сам называет ключи и синтаксис `.Роль.set(id)`. Ключ находит и имя класса
56
+ на конце, если роль называется иначе. Код ошибки прежний.
57
+ - `connect({ apiKey | service | token })` со значением `null`, `''` (или не строкой) — отказ `invalid_data`, ключ
58
+ `api.connect_empty_credential`, до подключения (раньше — подключение без сессии и `no_session` на первой записи;
59
+ частый случай — повторный `up()` вернул `serviceKey: null`). Опция со значением `undefined` — как прежде, опции нет.
60
+ Источник: ревью.
61
+
62
+ ### Логирование
63
+ - Журнал событий: `connect({ log, logLevel, redact })` и `up({ log, logLevel, redact })`. `log` — функция
64
+ `(event) => void` или объект с методами `debug`/`info`/`warn`/`error` (pino, `console`): вызов
65
+ `logger[level](event, event.event)`. `logLevel` — `'debug' | 'info' | 'warn' | 'error' | 'off'`, по умолчанию
66
+ `'info'`; ниже порога событие не собирается. `LETOPIS_LOG=<уровень>` — если `log` не передан, события пишутся
67
+ JSON-строками в stderr. CLI: `letopis <команда> --log[=уровень]`.
68
+ - Событие: `{ ts, level, event, op, ms, tenant, account, tx, chain, rows, code, key, detail }`, у ошибок ещё `cause`.
69
+ Секреты (пароли, токены, ключ сервиса, пароль в DSN, значения секретов) маскируются всегда; `redact: string[]` —
70
+ дополнительные имена полей. `onQuery` остаётся; каждый SQL — ещё и событие `sql` уровня `debug`.
71
+ - События: `connect`, `close`, `up`, `migrate`, `sync`, `read`, `cache.hit`, `cache.miss`, `write`, `delete.preview`,
72
+ `delete`, `restore`, `tx.begin`, `tx.commit`, `tx.rollback`, `tx.retry`, `auth.login`, `auth.logout`, `auth.revoke`,
73
+ `auth.switch`, `watch.start`, `watch.stop`, `signal`, `maintain`, `warning`, `error`, `sql`. Уровни: `write`,
74
+ `delete`, `restore`, `auth.*`, `up`, `migrate`, `sync`, `maintain` — `info`; `warning`, `tx.retry` — `warn`;
75
+ `error` — `error`, а у штатного отказа — `warn` (ниже); `read`, `cache.*`, `tx.begin`, `tx.commit`, `sql`,
76
+ `delete.preview` — `debug` (`tx.commit` транзакции с записью — `info`, ниже). Каталог событий —
77
+ `lib/src/messages/log.ts`, слой — `lib/src/log.ts`.
78
+ - Ошибка PostgreSQL в событии `error` (сама или причиной ошибки letopis, поле `cause`) — только SQLSTATE и имена объектов
79
+ схемы (`constraint`, `table`, `column`, `schema`, `datatype`, `routine`); её текст, `DETAIL`, `WHERE`, `HINT` и
80
+ внутренний запрос в журнал не пишутся: в них бывают значения параметров и данных («Key (email)=(…)»,
81
+ «invalid input syntax for type uuid: "…"»). У ошибки letopis параметры шаблона `value` и `text` в `msg` — `***`
82
+ (`query.bad_uuid`, `api.db_bad_value`, курсоры), у строк в `detail` (превью удаления `write.preview_changed`, снимки
83
+ `restore`) поле `data` — `***`. Сама ошибка, которую получает приложение, не меняется. Те же правила — у предупреждений
84
+ `time.maintain_failed` и `log.notify_failed`. Источник: ревью.
85
+ - Штатный отказ — событие `error` уровня `warn`: коды `exists`, `conflict`, `target_not_found`, `not_deleted`,
86
+ `delete_restricted`, `acl_denied`, `tenant_denied`, `no_session`, `duplicate`, `deleted`, `confirmation_required` и
87
+ `invalid_data` с нарушениями данных (`err.issues`); прочие — уровня `error`. Источник: ревью.
88
+ - Фиксация в журнале: в `db.begin()` события записи (`write`, `delete`, `restore`, `raw`, `acl.change`, `auth.*` с
89
+ записью) несут поле `tx`; `tx.commit` такой транзакции — уровня `info` с `detail.writes` (число операций записи),
90
+ без записи — `debug`; `tx.rollback` — `info` с `detail.writes`. Событие записи с `tx` фиксацию не доказывает —
91
+ доказывает `tx.commit` с тем же `tx`; вне `db.begin()` событие записи пишется после `COMMIT`. `tx.commit` у
92
+ `db.transaction(fn)` (сырой SQL) — всегда `info`. Источник: ревью.
93
+ - Событие `up` — `detail.schema` (фактическое имя схемы) и `detail.requested`, если имя передано без префикса версии
94
+ (`'salon'` → `'v2.salon'`); ответ `up()` — поле `schema`, как прежде. Источник: ревью.
95
+
96
+ ### Добавлено
97
+ - `delete({ confirm: true, all: true })` и `restore({ all: true })` — осознанное действие без сверки (см. «Переход на
98
+ 3.0»). Экспорт типов `DeleteOptions`, `RestoreOptions`.
99
+ - `syncModels` до записи проверяет имена ролей концов: имя роли одно на арендатора, и роль, занятая системным классом
100
+ (`User`, `Tenant` — класс `member`, ведут на `Account`; `Group`, `Object` — класс `rule`, ведут на `Resource`), может
101
+ вести только туда же. Иначе `invalid_class`, ключ `model.end_role_system` «имя роли Group в классе Item занято
102
+ системным классом rule (там Group ведёт на Resource) — выберите другое имя роли, например Team»; роль, которая в другом
103
+ классе арендатора ведёт на другой класс, — `model.end_role_taken` (раньше — только отказ базы
104
+ `sql-write.end_other_class` «конец Group в классе rule ведёт на другой класс»). Источник: ревью.
105
+ - `sort('code')` — имя поля данных без префикса — отказ с подсказкой «— может быть, data.code?» (ключ
106
+ `query.sort_field_data`); имя без `data.` не принимается: колонки `at`, `rev`, `id` и поле данных с тем же именем
107
+ разошлись бы. Источник: ревью.
108
+ - `api-contract.json` (машинный контракт API и ошибок) входит в пакет. Источник: ревью.
109
+
110
+ ### Изменено
111
+ - `versions()` — по-прежнему терминал (промис строк), но метод цепочки на его результате (`versions().rows()`,
112
+ `versions().sort(…)`) — `invalid_query`, ключ `api.versions_terminal`, с подсказкой «модификаторы — до
113
+ `versions()`» вместо `TypeError: … rows is not a function`. Источник: внедрение.
114
+ - Инструмент MCP `letopis_write` (op `delete`) удаляет с `expect` — превью, которое подтвердил человек; расхождение под
115
+ блокировкой — `conflict` (`write.preview_changed`), ничего не удалено. Параметры инструмента прежние.
116
+ - `db.acl.revoke()` и `db.acl.roles(account, null)` удаляют правила и членство как осознанное действие
117
+ (`{ confirm: true, all: true }`); поведение прежнее.
118
+ - Подсказки об удалённом арендаторе, пользователе и членстве в `createTenant` (`boot.tenant_deleted` и соседние) и кода
119
+ `not_deleted` называют `restore({ expect })` с надгробием из `withDeleted().rows()`.
120
+ - Текст кода `conflict` в каталоге перечисляет все случаи: `update` с `{ rev }` (`write.rev_conflict`),
121
+ `delete({ confirm: true, expect })` с изменившимся превью или исчезнувшими целями (`write.preview_changed`,
122
+ `detail.preview: []`), `restore({ expect })` объекта, который меняли после удаления (`write.restore_stale`).
123
+ Подсказка `acl_denied` называет `db.acl.grant` (класс `rule`). Источник: ревью.
124
+ - Описание пакета в `package.json` — по-русски; агенту — `AGENT-CHEATSHEET.md` и `llms.txt` в пакете, исходный код
125
+ доступен (PolyForm Noncommercial 1.0.0).
126
+
127
+ ### Удалено
128
+ - Ключ каталога `api.validation_failed_end` (заменён `write.link_in_data`) и предупреждение `write.restore_no_filter`.
129
+
130
+ ### Исправления
131
+ - Типы `hub(имя, поля)` и `link(имя, поля)` без третьего аргумента: короткая форма с полями и без опций проходит
132
+ проверку типов (раньше TypeScript выбирал перегрузку полного описания и отклонял поле как лишнее ключевое слово;
133
+ при выполнении форма работала). Источник: пример.
134
+ - Вход по ключу (`connect({ apiKey })`, `db.auth.verifyApiKey`, `db.auth.verifyKeySecret`) и отзыв ключа
135
+ (`db.auth.revokeCredential`) синхронизированы по строке ключа: вход берёт её `for share`, отзыв — `for update`. Раньше
136
+ сессия, открытая входом параллельно с отзывом, переживала отзыв: её токен оставался действующим. Теперь отзыв ждёт
137
+ фиксации такого входа и гасит его сессию, а вход после отзыва ключа не находит. Источник: ревью.
138
+ - Код OTP и фактор TOTP, отключённые `setFlags(id, { enabled: false })`, не принимаются: `verifyOtp` — `null`,
139
+ `verifyTotp` и вход с `totp` — отказ, `totpEnabled` — `false`, и для смены своих кредов отключённый TOTP не требуется.
140
+ Раньше проверка кодов флаг не учитывала, и верный код отключённого OTP выдавал сессию. Источник: ревью.
141
+ - `setFlags(id, { enabled: false })` гасит сессии отключённого креда (ключ — открытые им, прочие виды — сессии
142
+ аккаунта, как при отзыве) и берёт строку креда `for update`: вход по ключу, начатый параллельно, отключение дожидается
143
+ и гасит его сессию. Последний действующий ключ сервиса не отключается (`acl_denied`, как при отзыве). Раньше
144
+ сессии, выданные кредом до отключения, оставались действующими. Источник: ревью.
145
+ - Загрузчик сверяет цель вне загрузки со всеми ролями, где встречается её id: id объекта `Org` в ролях `OrgRef → Org`
146
+ и `StaffRef → Staff` — `target_not_found` по второй роли. Раньше проверялась только первая ссылка на id, неверная
147
+ вторая записывалась (триггеры загрузку не страхуют). Источник: ревью.
148
+ - Загрузчик: класс строки, удалённый между первым чтением реестра и блокировкой семейства, — отказ `invalid_class`
149
+ (`write.load_class_not_found`), ничего не записано. Раньше строки писались по прежнему описанию удалённого класса.
150
+ Источник: ревью.
151
+ - Слот со вложенной цепочкой с глаголом и читающим хвостом (`.OrgRef.set(db.Org(id).Ref().create(…).Org())`) получал
152
+ у непустого результата id `undefined` — конец не ставился. Теперь — id строк хвоста. Источник: ревью.
153
+ - Чтение семейства, где потомок переопределил тип поля (`Item.price` — число, `Gift.price` — строка `'free'`):
154
+ `sort('data.price')`, сравнения (`gt`, `between`, `inList` …) и агрегаты `min`, `max`, `sum`, `avg` больше не падают
155
+ ошибкой приведения. Если у классов шага тип поля разный, значение, которое не приводится к типу поля класса шага, —
156
+ `null`: при сортировке по возрастанию стоит после прочих, условию сравнения не отвечает, агрегат его пропускает;
157
+ приводимое (`'7'`) сравнивается как число. Курсор `after()` такой строки — как курсор `null`. Источник: ревью.
158
+ - `lostRules()` и `syncModels().lost` сообщают о проверках `z.object(…).refine()`, `.superRefine()`, `.check()` на
159
+ уровне объекта (путь — имя класса, `kind: 'refine'`); раньше они терялись молча. Источник: ревью.
160
+ - `paths()`: ключ повторного шага — первый свободный суффикс среди всех выданных ключей (алиасы `x`, `x_2`, `x` →
161
+ `x`, `x_2`, `x_3`). Раньше третий узел получал ключ `x_2` и затирал второй. Источник: ревью.
162
+
163
+ ## [2.1.0] — 2026-11-03
164
+
165
+ Правки по прогонам первых пользователей и агентов. Новые опции необязательны. Что меняется для работающего кода —
166
+ три места, каждое ниже отдельной строкой: **смысл `withDeleted()` не у последнего шага** (теперь — модификатор своего
167
+ шага), **ошибка PostgreSQL `22P02` приходит `LetopisError` с кодом `invalid_query`** (раньше — сырая ошибка
168
+ postgres.js, в том числе у `db.sql`) и **`syncModels` отклоняет преобразования Zod**. Прочие новые отказы — там, где
169
+ раньше была сырая ошибка PostgreSQL, `TypeError` или молчаливая ошибка (`db.begin(fn)` и неизвестная опция
170
+ `db.begin()`, гонка `delete({ confirm, expect })`). Ревизия движка 8 → 9: схему 2.0 обновляет
171
+ `up({ schema, upgrade: true })` (миграция `sql/migrate/0009-restore-rev.tenants-name.sql`).
172
+
173
+ ### Добавлено
174
+ - `restore({ expect })` — восстановить ровно то, что ушло удалением: `expect` — надгробия `{ id, rev }` из ответа
175
+ `delete()` или `withDeleted()`. Восстанавливаются только цели из `expect`, и только если последняя версия каждой — то
176
+ же надгробие; иначе ничего не восстановлено, а отказ называет причину (строка ниже). Номер версии база сверяет ещё
177
+ раз под блокировкой записи журнала: `restore(id, rev)` в SQL. Источник: внедрение.
178
+ - `restore({ expect, partial: true })` — вернуть удаление целиком, насколько можно: восстанавливаются цели, у которых
179
+ совпал `rev`, а остальные не роняют операцию и приходят в ответе строками `{ …строка, $action: 'skip', $why }`,
180
+ `$why` — `'restored'` (уже восстановлена), `'reissued'` (id занял новый объект с тем же ключом) или `'changed'`
181
+ (после показанного удаления её меняли: удалили снова и т. п.); строка-пропуск — последняя версия цели (живая или
182
+ надгробие). Проигранная гонка с другим `restore()` тоже становится пропуском. `partial` без `expect` — `invalid_query`
183
+ (`write.partial_without_expect`). Продолжение цепочки после `restore()` идёт только от восстановленных. Источник:
184
+ агенты.
185
+ - Отказы `restore()` — по смыслу, по записи журнала за последним надгробием цели (и в проверке до записи, и в базе под
186
+ блокировкой — проигравший гонку видит версию победителя):
187
+ - за надгробием `restore` — объект уже восстановлен: `not_deleted`, новый ключ `write.restore_already` (в базе
188
+ `sql-time.restore_already`);
189
+ - за надгробием `create` — id занял новый объект с тем же ключом (`create()` по ключу удалённого): `not_deleted`,
190
+ `write.restore_id_taken` (в базе `sql-time.restore_id_taken`);
191
+ - прочее (удалён снова с другим `rev`, не удалялся, гонка) — `conflict`, `write.restore_stale`.
192
+
193
+ У всех трёх `err.detail.ids` — id этого вида; с `expect` `err.detail.stale` — все цели, которые не восстановить:
194
+ `{ id, rev, expected, after, deleted, live, why }` (`rev` — последняя версия сейчас, `after` — вид записи журнала
195
+ сразу за `expected`, `null` — её нет; `deleted` — последняя версия — надгробие; `live` — объект живёт; `why` — как
196
+ `$why`). Несколько целей разного вида — `restore_stale`, если есть `changed`, иначе `restore_id_taken`, если есть
197
+ `reissued`. Источник: внедрение, агенты.
198
+ - `delete({ confirm: true, expect: превью })` — удалить ровно показанное: превью строится заново в транзакции удаления и
199
+ сравнивается с `expect` по `id`, `rev` и `$action`; расхождение — `conflict` (`write.preview_changed`, новое превью —
200
+ `err.detail.preview`). Атомарно под гонкой: всё замыкание каскада (строки превью с `$action: 'delete'`) блокируется
201
+ `for update` в порядке id, превью пересчитывается уже под блокировкой. Связь к удаляемой цели, вставленная раньше,
202
+ попадает в новое превью (`preview_changed`); вставленная позже ждёт удаления и получает `end_target_missing` —
203
+ непоказанное больше не уходит каскадом. Пустые цели: `expect: []` и целей нет — пустой ответ; `expect` непуст, а целей
204
+ нет (удалены, невидимы) — `conflict` с пустым `err.detail.preview`. `expect` без `confirm` — `invalid_query`.
205
+ Взаимоблокировку двух удалений повторяет операция из пула (`40P01`), в `db.begin()` — вызывающий. Источник:
206
+ внедрение.
207
+ - `db.auth.revoke(token)` — выход пользователя по токену его сессии (право `auth.session`, как у `session(token)`);
208
+ без аргумента — как прежде, выход подключения. Функция базы `auth_logout_token(text)`. Источник: внедрение.
209
+ - `db.auth.tenants()` — у арендатора ещё `name` (имя аккаунта арендатора) и `personal` (собственный арендатор).
210
+ Источник: внедрение.
211
+ - Путь через удалённый узел одной цепочкой: `db.Member(id).withDeleted().Loan().withDeleted()` — удалённые выдачи
212
+ удалённого участника (`withDeleted()` — модификатор своего шага, см. «Изменено»). Источник: внедрение.
213
+ - `versions()` шага с явными id отдаёт и историю удалённого объекта. Источник: внедрение.
214
+ - `connect({ schema: 'salon' })` — имя без точки дополняется до `v2.salon`, как у `up()`. Источник: агенты.
215
+ - Экспорт типа `Expected` (`{ id, rev, $action? }`) — строка `expect`.
216
+
217
+ ### Изменено
218
+ - **Изменение смысла: `withDeleted()` — модификатор своего шага, а не всей выборки.** У последнего шага — как прежде:
219
+ `db.Order(o).line().withDeleted()` — живые и удалённые строки живого заказа. Не у последнего — другой ответ:
220
+ - было: `db.Order(o).withDeleted().line()` — живые и удалённые **строки** живого заказа (флаг относился к последнему
221
+ шагу, где бы ни стоял);
222
+ - стало: тот же вызов — заказ с удалёнными, а **строки только живые**; на такой вызов выводится предупреждение
223
+ `api.with_deleted_step` (один раз на пару шагов).
224
+ - Код, который ставил `withDeleted()` раньше последнего шага, переносят в конец: `db.Order(o).line().withDeleted()`;
225
+ удалённые строки удалённого заказа — `db.Order(o).withDeleted().line().withDeleted()`. Опция `withDeleted`
226
+ инструмента MCP `letopis_query` — по-прежнему последнему шагу. В цепочке записи `withDeleted()` любого шага, кроме
227
+ шагов `restore()`, — `invalid_query`, как прежде у модификатора цепочки. Источник: внедрение.
228
+ - **Ошибка PostgreSQL `22P02` (значение не того типа) — `LetopisError` с кодом `invalid_query`.** Где бы она ни
229
+ возникла — в цепочке, в `db.sql`, `db.transaction(fn)` или `tr.sql` в `db.begin()`:
230
+ - было: сырая ошибка postgres.js, `err.code === '22P02'`, текст PostgreSQL (`invalid input syntax for type uuid:
231
+ "42"`);
232
+ - стало: `err instanceof LetopisError`, `err.code === 'invalid_query'`, `err.key === 'api.db_bad_value'`, текст —
233
+ `letopis: invalid_query: значение не того типа: invalid input syntax for type uuid: "42"`; исходная ошибка
234
+ PostgreSQL — в `err.cause` (`err.cause.code === '22P02'`).
235
+ - Проверку `err.code === '22P02'` меняют на `err.code === 'invalid_query'` (SQLSTATE — из `err.cause.code`). Неверный
236
+ id шага, фильтра `{ id }` или конца в цепочке до базы больше не доходит — строка о `query.bad_uuid` ниже. `40001` и `40P01`
237
+ приходят как прежде. Источник: агенты.
238
+ - Фильтр `update()`, суженный не только по id (данные, концы, `tags()`, `owner()`), перепроверяется: цели
239
+ блокируются (`for no key update`) и читаются заново, правятся только строки, которые после ожидания блокировки ещё
240
+ подходят. Без гонки — те же цели. Источник: внедрение.
241
+ - `syncModels` (и `letopis sync`) отклоняет преобразования Zod — `.trim()`, `.toLowerCase()`, `.transform()`,
242
+ `z.preprocess()`, `.pipe()`: база и валидатор их не исполняют, данные записались бы как есть (`invalid_class`,
243
+ `model.transform_unsupported`). `refine` по-прежнему в отчёте `lost`. Источник: агенты.
244
+ - Неверный id шага, фильтра `{ id }` или конца — `invalid_query` (`query.bad_uuid`) до запроса вместо сырой ошибки
245
+ PostgreSQL 22P02, `null` как id — тот же отказ вместо `TypeError`; id в любом виде, который принимает PostgreSQL,
246
+ проходит как прежде. Источник: агенты.
247
+ - Текст `invalid_data` называет первое нарушение («путь — сообщение») и число нарушений; сообщение нарушения
248
+ перечисления называет допустимые значения, лишнего ключа — сам ключ (`sql-core.issue_additional`,
249
+ `model.issue_additional`); лишний ключ — имя конца класса — подсказка «конец — из пути или `.Роль.set()`»
250
+ (`api.validation_failed_end`, в `update()` — до записи). У класса с правилами только для базы (`pattern`) ошибка до
251
+ записи несёт полный список нарушений — его даёт база (`validate_row`). Путь и ключевое слово нарушений не менялись.
252
+ Источник: агенты.
253
+ - Неизвестный шаг: подсказка о классах арендатора (`auth.tenants()` → `auth.switch()` / `db.as(…, { tenant })`); если в
254
+ реестре сессии нет ни одного класса арендатора — свой ключ `api.unknown_step_no_classes`; системные классы в
255
+ «похожие» не идут. `.length`, `.map()`, `[0]` и другие свойства массива у цепочки и её перебор — `invalid_query`
256
+ (`api.chain_not_rows`). Источник: агенты.
257
+ - Предупреждения (не отказы, `console.warn` один раз на место): шаг с аргументом `undefined`
258
+ (`api.step_undefined_filter`), шаг после `sort()` (`api.sort_not_last`), `restore()` без сужения и `expect`
259
+ (`write.restore_no_filter`), `withDeleted()` не у последнего шага (`api.with_deleted_step`). Источник: агенты.
260
+ - `undefined` в данных `create()` и `upsert()` отбрасывается, как в патче `update()`. Источник: агенты.
261
+ - `close()` у фасада `db.as()` ничего не закрывает: пул общий с корнем. Источник: агенты.
262
+ - Логин без крайних пробелов в `auth.setPassword()`, `auth.verifyPassword()` и `createTenant({ user })`; кред,
263
+ записанный раньше с пробелами, находится по логину как есть. Источник: агенты.
264
+ - Подсказка `no_session` неверного ключа сервиса: ключ перевыпускает `up({ fresh: true })`. Источник: агенты.
265
+ - `connect({ token })` с токеном, который не даёт сессии (погашен `auth.revoke`, истёк, неверен), — отклонённый промис
266
+ `no_session` (`api.token_not_active`), а не подключение, где первый же шаг давал `invalid_class` про личный арендатор.
267
+ Если сессия погасла после `connect()`, неизвестный шаг у фасада с токеном без арендатора сессии — тот же отказ, но
268
+ терминалом (отклонённым промисом), а не синхронным выбросом. Источник: внедрение.
269
+ - `db.begin(аргумент)` — только опции `{ reason }`: функция, строка, число, массив — `invalid_query`
270
+ (`api.begin_arg`, подсказка — форма `begin()` / `commit()` / `rollback()`); раньше `begin(fn)` функцию не вызывал,
271
+ а транзакция оставалась открытой. Неизвестная опция — `invalid_data` с похожими (`api.unknown_option`). Источник:
272
+ внедрение.
273
+ - Нарушение `minLength` / `maxLength` называет границу: «длина строки меньше {limit}» / «больше {limit}» (ключи
274
+ `model.issue_string_too_short`, `model.issue_string_too_long`, в базе `sql-core.issue_string_too_short`,
275
+ `sql-core.issue_string_too_long` вместо `*.issue_string_length`); путь и ключевое слово нарушения не менялись.
276
+ Источник: внедрение, агенты.
277
+ - `forUpdate()` с тем, что блокировать нельзя (`lock_unsupported`, `api.for_update_unsupported`), называет помеху —
278
+ терминал (`count()`, `paths()`, `versions()`, агрегат), `asOf()`, `withDeleted()`, `deep()` или глагол, — а подсказка
279
+ перечисляет допустимое и форму «прочитать с блокировкой, затем править». Источник: агенты.
280
+
281
+ ### Лицензия
282
+ - **Смена лицензии: MIT → PolyForm Noncommercial License 1.0.0** (SPDX `PolyForm-Noncommercial-1.0.0`, поле
283
+ `license` в `package.json`): исходный код открыт, некоммерческое использование бесплатно, коммерческое — по
284
+ разрешению владельца (`LICENSE-COMMERCIAL.md` в пакете: что считается коммерческим использованием и как получить
285
+ лицензию — issue на https://github.com/alepri51/letopis/issues). Новая лицензия действует с 2.1.0; версии до 2.0.0
286
+ включительно выпущены под MIT и остаются под ней. `LICENSE` в корне и в пакете — полный текст PolyForm
287
+ Noncommercial 1.0.0 со строкой `Required Notice: Copyright 2026 alepri51 (https://github.com/alepri51)`; раздел
288
+ «Лицензия» в README, строки в шпаргалке и `llms.txt`. `check:docs` узнаёт лицензию по заголовку текста.
289
+
290
+ ### Документация
291
+ - `README.md`, `AGENT-CHEATSHEET.md`, `llms.txt` — под поведение 2.1: `restore({ expect })` и
292
+ `delete({ confirm, expect })`, выход по токену `auth.revoke(token)` с рецептом веб-сервиса, `auth.tenants()` с
293
+ `name` и `personal` и рецепт «арендаторы пользователя для сервиса», `withDeleted()` по шагу (таблица цепочек),
294
+ `versions()` удалённого и форма версии (`op`, `links`, `data`, `$deleted`, порядок без `sort()`), перепроверка
295
+ фильтра `update()`, `connect({ schema: 'имя' })`, отказ преобразований Zod, предупреждения, новые ключи ошибок,
296
+ `22P02` — `invalid_query` (раздел «Ошибки» и «Сырой SQL»). Удалено «id не uuid — сырая ошибка 22P02».
297
+ - Удаление и восстановление (README, раздел 7): занятость держит ключ связи; `create()` по ключу удалённого занимает
298
+ его id — перед `create()` проверка `X({ ключ }).withDeleted()`; надгробия для `expect` — из ответа `delete()` или
299
+ `withDeleted()` (строки `asOf` — живые версии); цель и каскад — в одной `db.begin()`; причина удаления — своя у
300
+ каждого удаления; таблица штатных ответов `restore()` (`not_deleted`, `restore_stale`, `target_not_found`, пустой)
301
+ с сообщением пользователю «вещь уже у другого».
302
+ - Рецепты: `syncModels` в нескольких арендаторах, текст для пользователя из `err.issues`.
303
+ - Шпаргалка (`AGENT-CHEATSHEET.md`), раздел 6 — готовый рецепт «одна открытая выдача на вещь (ключ связи по вещи),
304
+ удаление участника по превью `delete({ confirm, expect })`, возврат `restore({ expect, partial: true })` с показом
305
+ пропусков `$why`» и вариант без `partial` с разбором `err.detail.stale`; форма превью и `err.detail.preview`, форма
306
+ `expect`; новый раздел «Веб-сервис безопасно»: арендатор только из серверной сессии, одно подключение сервиса и
307
+ `svc.as(account, { tenant })` на запрос вместо `connect({ token })` на каждый вход, экранирование `& < > " '` без
308
+ обработчиков из данных, ключ сервиса — файл с правами владельца вне репозитория (то же — README, раздел 10, и
309
+ `llms.txt`).
310
+ - `versions()` по пути — только живые объекты (шпаргалка, раздел 10), история вещи, пока участник удалён, и что видно
311
+ через `withDeleted()`, когда id надгробия занят новой записью; `createTenant` — аргументы и ответ; `db.idOf` по концу
312
+ связи; `syncModels` через `svc.as()` без администраторского подключения; «Грабли» — `.nullable()`: `null`, а не
313
+ `undefined`.
314
+ - Отказы второго раунда описаны в README («Восстановить: `restore`», «Строгий режим», «Ошибки»), шпаргалке и
315
+ `llms.txt`: `write.restore_already`, `write.restore_id_taken` и `write.restore_stale` по смыслу с `err.detail.stale`
316
+ (`after`, `live`, `why`), `write.partial_without_expect`, `api.begin_arg`, `api.token_not_active` у `connect({ token })`,
317
+ граница длины строки в тексте нарушения, `api.for_update_unsupported` с помехой; атомарность
318
+ `delete({ confirm, expect })` под гонкой.
319
+
320
+ ### Ревизия движка 9
321
+ - `restore(uuid, int default null)` вместо `restore(uuid)`; `auth_tenants()` — ещё `name`, `personal`; новые
322
+ `auth_logout_token(text)`, `validate_row(text, jsonb)`, `issue_values(jsonb)`; `validate_data` — сообщения с
323
+ допустимыми значениями и лишними ключами, граница длины строки.
324
+ - `restore()`: последняя запись журнала перечитывается после ожидания блокировки; отказы `sql-time.restore_already`
325
+ (новый), `sql-time.restore_id_taken` (`detail` — `{ id, rev }`), `sql-time.restore_stale` (`detail` —
326
+ `{ id, rev, expected, after, deleted, live }`).
327
+
7
328
  ## [2.0.0] — 2026-11-03
8
329
 
9
330
  План 2.0, редакция 2 (`reports/PLAN-LETOPIS-2.0.md`, §2.1): 1.5.0 не выпускается — его работа (этап 3: права, один
package/LICENSE CHANGED
@@ -1,21 +1,75 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 alepri51
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Required Notice: Copyright 2026 alepri51 (https://github.com/alepri51)
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
@@ -0,0 +1,49 @@
1
+ # Коммерческое использование letopis
2
+
3
+ letopis начиная с версии 2.1.0 распространяется под лицензией **PolyForm Noncommercial License 1.0.0** (SPDX
4
+ `PolyForm-Noncommercial-1.0.0`), полный текст — в файле [LICENSE](LICENSE) и на
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>. Исходный код открыт, некоммерческое использование
6
+ бесплатно; коммерческое — только по отдельной лицензии от владельца. Это не лицензия open source в смысле OSI.
7
+
8
+ Юридическую силу имеет английский текст [LICENSE](LICENSE). Этот файл — пояснение к нему, а не его замена.
9
+
10
+ ## Что можно без разрешения
11
+
12
+ Лицензия разрешает любое использование для некоммерческих целей, в том числе:
13
+
14
+ - личное — для исследований, экспериментов и тестирования на благо общего знания, учёбы, частного досуга, хобби и
15
+ любительских проектов, если коммерческого применения не предполагается;
16
+ - в некоммерческих организациях — благотворительных, учебных заведениях, государственных исследовательских
17
+ организациях, организациях общественной безопасности и здравоохранения, охраны окружающей среды и государственных
18
+ учреждениях, — независимо от того, откуда у них финансирование.
19
+
20
+ В этих пределах код можно запускать, менять, писать на его основе свои работы и распространять копии — с текстом
21
+ лицензии (или ссылкой на него) и строкой `Required Notice:` из [LICENSE](LICENSE).
22
+
23
+ ## Что требует разрешения
24
+
25
+ Всё, что не подпадает под некоммерческие цели выше, — коммерческое использование. Например:
26
+
27
+ - продукт или сервис, который продают или за доступ к которому берут плату, в том числе SaaS и облачный сервис;
28
+ - внутренние системы коммерческой компании — учёт, CRM, бэкенд сайта или приложения, — даже если сам продукт
29
+ бесплатен для пользователей;
30
+ - разработка на заказ: проект для клиента, работа подрядчика или фрилансера;
31
+ - встраивание letopis или его частей в платный или коммерчески распространяемый продукт.
32
+
33
+ Сомневаетесь, коммерческое ли ваше использование, — спросите владельца тем же способом, что ниже.
34
+
35
+ ## Как получить коммерческую лицензию
36
+
37
+ Связь с владельцем — только через GitHub:
38
+
39
+ 1. Откройте issue в репозитории: <https://github.com/alepri51/letopis/issues> — с заголовком «Коммерческая лицензия».
40
+ 2. Опишите, кто вы (компания или проект), как вы используете letopis и в каком масштабе (сервисы, пользователи,
41
+ арендаторы).
42
+ 3. Владелец ответит в issue. Профиль владельца — <https://github.com/alepri51>.
43
+
44
+ Issue на GitHub публичный: не пишите в нём ничего конфиденциального.
45
+
46
+ ## Прежние версии
47
+
48
+ Версии letopis до 2.0.0 включительно выпущены под лицензией MIT и остаются под ней: смена лицензии действует с версии
49
+ 2.1.0.