letopis 1.1.0 → 1.2.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.
- package/AGENT-CHEATSHEET.en.md +110 -15
- package/AGENT-CHEATSHEET.md +103 -13
- package/CHANGELOG.md +577 -0
- package/README.en.md +769 -73
- package/README.md +732 -67
- package/dist/admin.d.ts +20 -4
- package/dist/admin.js +33 -9
- package/dist/cli.js +53 -4
- package/dist/errors.js +4 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +75 -17
- package/dist/internal.d.ts +16 -0
- package/dist/internal.js +14 -0
- package/dist/load.js +42 -11
- package/dist/mcp.d.ts +28 -0
- package/dist/mcp.js +810 -0
- package/dist/model.d.ts +34 -0
- package/dist/model.js +17 -0
- package/dist/project.d.ts +309 -0
- package/dist/project.js +1097 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +13 -0
- package/dist/sql.js +9 -5
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/uuid.js +19 -8
- package/dist/watch.d.ts +12 -0
- package/dist/watch.js +20 -1
- package/dist/write.d.ts +1 -1
- package/dist/write.js +25 -5
- package/llms.txt +6 -2
- package/package.json +12 -2
- package/sql/10-core.sql +5 -1
- package/sql/15-errors.sql +4 -1
- package/sql/40-class.sql +57 -2
- package/sql/60-write.sql +515 -48
- package/sql/70-read.sql +33 -8
- package/sql/80-auth.sql +4 -4
- package/sql/88-derived.sql +1154 -0
- package/sql/90-time.sql +89 -12
- package/sql/95-seed.system.sql +12 -2
- package/sql/README.md +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,583 @@
|
|
|
2
2
|
|
|
3
3
|
Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver.
|
|
4
4
|
|
|
5
|
+
## [1.2.0] — 2026-10-28
|
|
6
|
+
|
|
7
|
+
letopis 1.2 по плану `reports/PLAN-LETOPIS-1.1.md` (редакция 5, этапы 4–6 и контрольная точка Б) и плану доработок
|
|
8
|
+
после точки Б: проекторы из журнала, агрегаты в базе, инструменты MCP; ревью безопасности №2 и №3. Версия `1.2.0` (во
|
|
9
|
+
время работы — `1.2.0-alpha.0`, не публиковалась). Схема 1.1.0 обновляется повторным `up()`: ревизия движка прежняя
|
|
10
|
+
(6), функции накатываются заново. Первый `up()` строит индекс журнала `log_class_tenant_at (class, tenant, at)` вместо
|
|
11
|
+
`log_class_at`, и пока индекс строится, запись в журнал ждёт: на большой установке постройте его заранее
|
|
12
|
+
(`create index concurrently`, README, «Обновление пакета и схемы»). Обновление схемы 1.1.0 с данными проверено разовым
|
|
13
|
+
прогоном (установка пакетом 1.1.0, `up()` 1.2, вход, чтение, запись, ужесточение, `purge` удалённого в 1.1,
|
|
14
|
+
`maintain()`, `verify({ data: true })`); в набор тестов оно не входит.
|
|
15
|
+
|
|
16
|
+
### Added — этап 4а (проекторы)
|
|
17
|
+
- `db.projector(имя, { from, into, version, handle, withBefore, onExpired, batch, pollMs })` — проектор из журнала
|
|
18
|
+
(`lib/src/project.ts`): строит строки производных классов из изменений классов-источников. `run()` догоняет
|
|
19
|
+
журнал и следует за ним до `stop()`, `run({ once: true })` только догоняет; `rebuild()` и `rebuild({ chunked: true })`
|
|
20
|
+
перестраивают проекцию с текущего состояния; `status()` отдаёт состояние, версию, курсор и отставание; `close()`
|
|
21
|
+
останавливает проектор и отписывает его от сигналов. Каждая пачка — одна транзакция: блокировка проектора,
|
|
22
|
+
чекпоинт, изменения журнала, `handle(changes, out)`, запись результатов и сдвиг курсора. Каждое изменение
|
|
23
|
+
применяется ровно один раз, писатель у проектора один. Только на администраторском подключении (иначе
|
|
24
|
+
`admin_required`).
|
|
25
|
+
- Опция класса `derivedBy: 'имя-проектора'`: строки класса пишет только этот проектор. Опция наследуется, потомок её
|
|
26
|
+
не меняет; концы производного класса — только `onDelete: 'cascade'` или `'unset'`; системный класс производным не
|
|
27
|
+
бывает; концы других классов на производный класс не ведут. Опцию показывают реестр, `db.describe()` и
|
|
28
|
+
`describeText()`.
|
|
29
|
+
- Функции базы `lib/sql/88-derived.sql` (только для роли владельца): `projector_state` (чекпоинт — строка `secret`
|
|
30
|
+
вида `PROJECTOR`), `projector_advance` (сдвиг чекпоинта, только если курсор не изменился), `derived_split`,
|
|
31
|
+
`derived_id`, `derived_apply` (запись результатов пачки: `upsert`, `inc`, `delete`), `derived_clear` (удаление
|
|
32
|
+
строк проектора перед перестройкой).
|
|
33
|
+
- Код ошибки базы `LT021` `derived_class`: строку производного класса пишет, правит или удаляет не его проектор,
|
|
34
|
+
либо конец ведёт на такую строку.
|
|
35
|
+
- Коды библиотеки `rebuild_required` (проекция построена другой версией проектора) и `projector_busy` (перестройку
|
|
36
|
+
не начать: проектор сейчас работает в другом процессе).
|
|
37
|
+
- Зарезервированное имя корня `projector`; типы `Projector`, `ProjectorOptions`, `ProjectorChange`, `ProjectorOut`,
|
|
38
|
+
`ProjectorRow`, `ProjectorReport`, `ProjectorStatus`.
|
|
39
|
+
|
|
40
|
+
### Changed — этап 4а
|
|
41
|
+
- Функции кредов (`auth_credentials`, `auth_revoke_credential`, `auth_set_flags`) не видят вид `PROJECTOR`, а
|
|
42
|
+
`auth_kind_ok` не принимает его как вид кредов.
|
|
43
|
+
- `targets_bad` отличает производную цель: возвращает `!роль`, и триггеры записи, `check_links` и проверка базы в
|
|
44
|
+
загрузчике отвечают `derived_class`, а не `target_not_found`.
|
|
45
|
+
- Загрузчик (`db.load()` и первая заливка) отклоняет строки производных классов и ссылки на их строки —
|
|
46
|
+
`derived_class` в отчёте.
|
|
47
|
+
- `restore`, `reclass`, `rekey` и `rekeyClass` строк производного класса — `derived_class`.
|
|
48
|
+
- Ревью коммита: проектор работает с арендаторами, у которых каждый класс `into` помечен его `derivedBy`, и
|
|
49
|
+
пропускает изменения остальных — одноимённый класс без пометки у другого арендатора больше не останавливает его для
|
|
50
|
+
всех; класс System, который становится производным, сверяется со ссылками всех арендаторов; описание со ссылкой и
|
|
51
|
+
пометка `derivedBy` идут под блокировками семейства и не проходят параллельно мимо друг друга; источники и
|
|
52
|
+
результаты проектора — свои у каждого арендатора: имя класса одного арендатора не меняет набор источников другого.
|
|
53
|
+
- Правка `derivedBy` в описании класса — не безопасное изменение (`class_change_safe`): её проверяют как ужесточение.
|
|
54
|
+
|
|
55
|
+
### Tests — этап 4а
|
|
56
|
+
- `projector` (план 1.1, §7.5 №1–11 и гонка №19 §7.8): догонка 10 000 изменений, ровно один раз при обрыве
|
|
57
|
+
посреди пачки, один писатель, долгая пишущая транзакция, транзакция на границе снимка перестройки, запрет записи
|
|
58
|
+
в производные классы и ссылок на них, границы `out`, перестройка атомарная и пачками во время записи источника,
|
|
59
|
+
`purge` источника служебным изменением, отставание старше яруса `all` (`cursor_expired`, `onExpired: 'rebuild'`,
|
|
60
|
+
молчащий проектор не истекает), функции кредов; версия, имя, администраторское подключение, повторный `up()`;
|
|
61
|
+
два процесса одного проектора при параллельной записи источника.
|
|
62
|
+
- `engine-attacks` №33–34 (план 1.1, §7.7): запись в производный класс и ссылка на него — `derived_class`, описание
|
|
63
|
+
класса со ссылкой на производный отклонено; чекпоинт проектора через функции кредов — строк нет в ответах,
|
|
64
|
+
изменить его нельзя.
|
|
65
|
+
- `scenario-salon`: матрица покрытия API исключает `projector` — его проверяет набор `projector`.
|
|
66
|
+
- `initial-load` 7.4 №5: загрузка стартует после вставки транзакции `early`, как `holdRow` в `write-races`. В CI
|
|
67
|
+
загрузка иногда брала блокировку первой и проходила по пустой таблице без `not_empty`. `lt()` сообщает
|
|
68
|
+
«ожидалась ошибка X, а операция прошла», если операция не бросила, вместо `TypeError` на `null`.
|
|
69
|
+
|
|
70
|
+
### Added — этап 4б (агрегаты в базе)
|
|
71
|
+
- Опция класса `aggregate: { поле: { sum: 'поле' | count: true, of: 'Класс', via: 'роль', where?: JSON Schema } }`:
|
|
72
|
+
поле держит сама база — сумма поля или число живых строк класса-источника (с потомками) того же арендатора, чей
|
|
73
|
+
одиночный конец `via` ведёт на строку и чьи данные проходят `where`. Строка System в агрегат не входит. Опция
|
|
74
|
+
наследуется, потомок заменяет её целиком; её показывают реестр (`ClassInfo.aggregate`), `db.describe()` и
|
|
75
|
+
`describeText()`; в `letopis/model` — `ClassDef.aggregate` и тип `AggregateDef`, экспорт типа `AggregateInfo`.
|
|
76
|
+
- Триггеры `entity_total_ai`, `entity_total_au`, `entity_total_ad` (`lib/sql/88-derived.sql`): после оператора, с
|
|
77
|
+
переходными таблицами, без условия `WHEN` — срабатывают и для владельца (каскад удаления, `restore`, `reclass`,
|
|
78
|
+
`rekey`, удаление аккаунта, загрузчик), `enable always`. Вклады строк оператора складываются по цели, цели
|
|
79
|
+
блокируются в порядке id, итог проверяет описание класса цели (`minimum: 0` → `invalid_data`), версия цели —
|
|
80
|
+
`sys_update` с автором и причиной операции. Новая цель оператора (вставка, `restore`, смена класса) считается
|
|
81
|
+
целиком из живых источников в ту же версию. Перенос строки (`move_row`) копит вклады до своего конца и применяет их
|
|
82
|
+
одним итогом — промежуточная сумма не выходит за правило класса.
|
|
83
|
+
- Проверки описания агрегата (`invalid_class`): поле есть в схеме, числовое и не в ключе; ровно одно из `sum` и
|
|
84
|
+
`count`; конец `via` источника одиночный и принимает класс цели; поле `sum` — число; нет цикла агрегатов; цель и
|
|
85
|
+
источник не системные и не производные; агрегат арендатора не считает классы System. Источник, описанный после
|
|
86
|
+
цели, сверяется при появлении; его класс получает метку `resolved.aggregatedBy`.
|
|
87
|
+
- `db.rebuildAggregates(класс)` — ремонт: поле класса и его потомков во всех арендаторах заново из живых источников
|
|
88
|
+
под исключительными блокировками, версия только у исправленных; возвращает их число. Только администраторское
|
|
89
|
+
подключение (иначе `admin_required`); функция владельца `rebuild_aggregates`. Зарезервированное имя корня
|
|
90
|
+
`rebuildAggregates`.
|
|
91
|
+
- `verify({ data: true })` сообщает расхождения агрегатов: `{ id, class, field, stored, expected, problem }`.
|
|
92
|
+
- Код ошибки базы `LT022` `aggregate_field`: приложение передало или изменило агрегатное поле.
|
|
93
|
+
|
|
94
|
+
### Changed — этап 4б
|
|
95
|
+
- Агрегатное поле пишет только база: `trg_entity_bi` отклоняет переданное значение и ставит 0 до умолчаний и
|
|
96
|
+
проверки, `trg_entity_bu` отклоняет изменённое, а при замене данных целиком без поля возвращает прежнее значение;
|
|
97
|
+
`reclass()` и `rekey()` отклоняют его в данных. Библиотека отвечает `aggregate_field` до отправки (`create`,
|
|
98
|
+
`upsert`, `update`, `reclass`, `rekey`), а `upsert` заменяет данные без агрегатных полей
|
|
99
|
+
(`excluded.data - поля`).
|
|
100
|
+
- Добавить или изменить агрегат — ужесточение (`class_change_safe`): исключительные блокировки семейства цели и
|
|
101
|
+
семейств источников, пересчёт поля у всех целей (`class_evolve`), метки источников заново (`agg_mark`).
|
|
102
|
+
- Загрузчик ставит агрегатные поля в 0 (поле посчитает триггер), при `existing: 'update'` сохраняет прежние значения
|
|
103
|
+
(`agg_keep`) и пишет журнал пачки раньше строк: новая цель получает итог в ту же версию. Первая заливка считает
|
|
104
|
+
поле функцией `agg_fill` — одним оператором на агрегат до перестройки индексов, в ту же версию в `entity` и `log`.
|
|
105
|
+
- `restore()` и `sys_update` со сменой класса пишут в журнал строку таблицы, а не `returning`: агрегатные поля
|
|
106
|
+
досчитаны триггером; `sys_create` пишет журнал раньше строки. `reset({ level: 'tenant' })` агрегаты не
|
|
107
|
+
пересчитывает: строки арендатора уходят вместе с целями.
|
|
108
|
+
- Ревью коммита этапа 4б: агрегат считают функции владельца по всем строкам источника, без прав чтения, и описание с
|
|
109
|
+
`where` выдавало суммы и счётчики по строкам, которые автору не видны. Теперь строки попадают в агрегат только с
|
|
110
|
+
ведома того, кто читает их все. Описать или изменить агрегат может только тот, у кого право READ без условий на все
|
|
111
|
+
строки каждого класса-источника (класс `of` и его потомки) — и свой агрегат, и унаследованный (новый потомок цели,
|
|
112
|
+
смена `parent`); неизменённый агрегат при правке описания не проверяется. Сделать класс источником (новый класс, смена `parent`, источник после цели) можно только с таким правом
|
|
113
|
+
на этот класс и его потомков — до правки, по прежним предкам, и после. Иначе `acl_denied` (`class_admit`, а для
|
|
114
|
+
нового класса и новых предков — `agg_source_admit` в триггерах после оператора и в `class_evolve`). Агрегат класса
|
|
115
|
+
арендатора считает только классы своего арендатора — везде одинаково (`agg_prepare`, `agg_classes`, `agg_compute`,
|
|
116
|
+
`agg_apply`, `agg_fill`, `agg_cycle`): класс System с именем `of` его источником не бывает. Поэтому такой агрегат
|
|
117
|
+
больше не останавливает установку и правку классов System (`agg_source` его пропускает, а не отвечает
|
|
118
|
+
`invalid_class`), не расходится с `verify` и `rebuildAggregates` и не мешает арендатору править цель.
|
|
119
|
+
|
|
120
|
+
### Tests — этап 4б
|
|
121
|
+
- `aggregate` (план 1.1, §7.5 №12–16, №18–20 и гонка №18 §7.8): описание агрегата в реестре и его проверки;
|
|
122
|
+
20 соединений пишут операции одного кошелька; `insert`, `update` и `delete` сырым SQL; каскад, `reclass` (с
|
|
123
|
+
переносом id и без), `restore` операции, загрузчик, удаление аккаунта; `restore` кошелька после удаления части
|
|
124
|
+
операций, `rekey` и `reclass` кошелька — одна версия без поправок; агрегат добавлен под параллельной записью;
|
|
125
|
+
смена описания и параллельные операции; `aggregate_field`; порча владельцем, `verify` и `rebuildAggregates`;
|
|
126
|
+
строка System.
|
|
127
|
+
- `initial-load` 7.4 №10: первая заливка в схему с агрегатом — итог равен сумме загруженных операций, одна версия,
|
|
128
|
+
журнал совпадает со строкой, `verify` чист.
|
|
129
|
+
- `roles` №4 и №7: триггерные функции агрегатов приложению не выданы; триггеры `entity_total_*` — `enable always`
|
|
130
|
+
без условия `WHEN`. `engine-attacks` №19 включает их обратно вместе с остальными. `scenario-salon`: матрица
|
|
131
|
+
покрытия исключает `rebuildAggregates` — его проверяет набор `aggregate`.
|
|
132
|
+
|
|
133
|
+
### Added — этап 5 (MCP)
|
|
134
|
+
- Подпуть `letopis/mcp`: `createMcpServer(db, { write, maxRows, maxBytes })` (`lib/src/mcp.ts`) — сервер MCP для
|
|
135
|
+
одного пользователя letopis. Инструменты: `letopis_model` (модель: классы, поля, `anyOf`/`oneOf`, концы, ключи,
|
|
136
|
+
алиасы, метаданные; с `class` — один класс), `letopis_query` (чтение JSON-цепочкой: старт `class` и `filter`, `id`
|
|
137
|
+
или `ids`, `steps` по ролям и классам, `asOf`, `withDeleted`, `sort`, `limit`, `cursor`; терминалы `rows`, `first`,
|
|
138
|
+
`ids`, `count`, `sum`, `avg`, `min`, `max`, `countBy`), `letopis_history` (`versions`, `diff`, `blame`) и при
|
|
139
|
+
`write: true` — `letopis_write` (`create`, `update`, `upsert`, `delete`). Цепочка собирается вызовами API, без
|
|
140
|
+
`eval` и без SQL; операторы фильтра JSON (`$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$like`, `$ilike`,
|
|
141
|
+
`$starts`, `$ends`, `$has`, `$hasAny`, `$hasAll`, `$exists`, `$isNull`, `$not`, `$or`) — фабрики `ops.ts`.
|
|
142
|
+
- Ответ — `structuredContent` и его текстовый дубль: не больше `maxRows` строк (100) и `maxBytes` байт (50 000),
|
|
143
|
+
дальше — свой курсор `cursor` (сортировка и значение последней строки; без `sort` — по `id`). Всё, что пришло из
|
|
144
|
+
базы, в тексте — внутри `<untrusted-data-…>` со случайным суффиксом и пояснением; описания инструментов и
|
|
145
|
+
`instructions` — постоянный текст пакета, `meta` классов — только в результатах. Неизвестный класс, роль, параметр,
|
|
146
|
+
оператор — `isError` с подсказкой похожих имён (расстояние Левенштейна по классам, алиасам и ролям реестра).
|
|
147
|
+
- Запись через MCP: `update` — только с `rev`; `delete` без `confirm` — превью и код операции, с `confirm` —
|
|
148
|
+
подтверждение человека через elicitation (ревизия 2025 — запрос клиенту, 2026-07-28 — `input_required` с
|
|
149
|
+
подписанным `requestState`), привязанное к хэшу операции; превью и удаление — одной транзакцией. Причина каждой
|
|
150
|
+
записи — `mcp: letopis_write`.
|
|
151
|
+
- Команда `letopis mcp` (stdio): `--dsn`, `--schema`, `--token` или `LETOPIS_DSN`, `LETOPIS_SCHEMA`, `LETOPIS_TOKEN`;
|
|
152
|
+
`--write`; только токен сессии пользователя. Сервер — `serveStdio` SDK: ревизию протокола выбирает клиент.
|
|
153
|
+
- Необязательная peer-зависимость `@modelcontextprotocol/server` ^2.4.0 (dev — вместе с `@modelcontextprotocol/client`
|
|
154
|
+
для тестов): основная точка входа и CLI её не загружают, `letopis/mcp` грузит её динамическим `import()`; нет
|
|
155
|
+
пакета — ошибка с командой установки.
|
|
156
|
+
- Коды библиотеки: `user_session_required` — `createMcpServer` без сессии пользователя или от корня сервиса без
|
|
157
|
+
`as()`, `letopis mcp` с ключом сервиса или без токена; `confirmation_required` — удаление через MCP без подтверждения
|
|
158
|
+
человека.
|
|
159
|
+
|
|
160
|
+
### Changed — этап 5
|
|
161
|
+
- `createMcpServer` отклоняет подключение в обход RLS (`bypass_rls`): признак — внутренний модуль `lib/src/internal.ts`
|
|
162
|
+
(фасад → свойства подключения; `makeDb` регистрирует каждый фасад, в `exports` модуля нет), а не слова вызывающего;
|
|
163
|
+
корень сервиса без `as()` и подключение без сессии — по `ctx()` базы (`user_session_required`); фасад `db.begin()` —
|
|
164
|
+
`invalid_query`.
|
|
165
|
+
- CLI: строгий `parseArgs` принимает `--write`; `letopis mcp` подставляет `LETOPIS_*` до проверки `--dsn` и
|
|
166
|
+
`--schema`, отклоняет `--api-key` (код 2), загружает `mcp.js` и SDK лениво и держит процесс до закрытия stdin.
|
|
167
|
+
В stdout — только протокол.
|
|
168
|
+
- Отступление от плана: имена SDK 2.4.0 сверены по типам — `McpServer`, `fromJsonSchema`, `InMemoryTransport`,
|
|
169
|
+
`createMcpHandler`, `serveStdio` (`@modelcontextprotocol/server/stdio`), `inputRequired`/`inputResponse`. В ревизии
|
|
170
|
+
2026-07-28 подтверждение идёт ответом `input_required` (запрос клиенту `elicitInput` там отключён), поэтому сервер
|
|
171
|
+
поддерживает оба пути. Обработчик HTTP в ревизии 2025 без состояния возможностей клиента не знает — удаление по нему
|
|
172
|
+
отклоняется (`confirmation_required`).
|
|
173
|
+
|
|
174
|
+
### Tests — этап 5
|
|
175
|
+
- `mcp` (план 1.1, §7.6 №1–12): модель и `meta` только в результатах; права — пользователь арендатора A не видит
|
|
176
|
+
строк и классов B; JSON-цепочка — шаги по ролям и классам, операторы, неизвестный класс, роль, параметр и оператор,
|
|
177
|
+
зарезервированные имена — `isError` с подсказкой в обёртке; 1000 строк — 100 и курсор, обход всех страниц,
|
|
178
|
+
сортировка по полю данных, предел байт; строка с текстом-инструкцией и поддельной меткой — внутри обёртки, суффикс
|
|
179
|
+
меняется; без `write` инструмента записи нет; `update` без `rev` — отказ, чужая версия — `conflict`; удаление —
|
|
180
|
+
превью, подтверждение человека в ревизиях 2025 и 2026-07-28, отказ человека, код операции от агента, клиент без
|
|
181
|
+
elicitation; причина в `blame`, `versions`, `diff`; учётные данные — `letopis mcp --api-key` и с токеном сервиса,
|
|
182
|
+
подключение в обход RLS, корень сервиса, подключение без сессии, `db.begin()`, `svc.as(пользователь)`; схемы
|
|
183
|
+
инструментов; `index.ts` и `cli.ts` не тянут `mcp.ts` и SDK статически. Дополнительно: `letopis mcp` по stdio
|
|
184
|
+
(клиент SDK запускает команду, `LETOPIS_*`, `--write`) и обработчик HTTP `createMcpHandler` (ревизии 2025 и
|
|
185
|
+
2026-07-28, удаление с подтверждением через новый экземпляр сервера на запрос).
|
|
186
|
+
- `engine-attacks` №35 (§7.7): агент через MCP пишет без `write` и удаляет без подтверждения человека — отказ, строка
|
|
187
|
+
цела.
|
|
188
|
+
- `docs`: сценарий `mcp` README (обе версии) — обработчик HTTP из примера, клиент SDK через `fetch` приложения;
|
|
189
|
+
`letopis/mcp` в примерах — исходники пакета.
|
|
190
|
+
|
|
191
|
+
### Changed — этап 6
|
|
192
|
+
- Документация — по проверке понятности (§7.9): агент, видевший только README, решил все пять задач, но угадывал
|
|
193
|
+
форму ответов. README (обе версии): `diff` и `blame` — пример ответа, форма `from`, `to`, `changes` и `fields`
|
|
194
|
+
(`path` — массив сегментов, листья по порядку пути, пустые `agent`, `reason`, `before`, `after` опускаются), типы
|
|
195
|
+
`DiffResult`, `DiffChange`, `BlameResult`, `BlameField` — и в «Экспортах пакета»; сигналы — канал с полным именем
|
|
196
|
+
схемы (`LISTEN "v2.shop"`), нагрузка по режимам, служебные `session:<16 hex>`, `session:*` и `*`; проекторы — когда
|
|
197
|
+
`before` равен `null` и когда его нет вовсе, значения `status().state`; MCP — `structuredContent` по терминалам
|
|
198
|
+
`letopis_query`, отказ (`isError`, `structuredContent.error`), `sort: { field, dir }`, вызов `letopis_write` без
|
|
199
|
+
`write` — ошибка протокола SDK, проверка обработчика HTTP клиентом SDK без сети (как в тесте `docs`). Шпаргалка
|
|
200
|
+
(обе версии) — то же коротко.
|
|
201
|
+
- Сверка документации с кодом 1.2 (план 1.1, §5, этап 6, п. 1 и 5). README (обе версии): во введении — производные
|
|
202
|
+
данные, MCP и `diff`/`blame`, в требованиях — необязательная peer-зависимость `@modelcontextprotocol/server`; `secret`
|
|
203
|
+
хранит и чекпоинты проекторов; администраторское подключение нужно и проекторам, и `rebuildAggregates`; ревизия
|
|
204
|
+
движка — 6, а не 5. Новые рецепты: «Баланс агрегатом» (§8) — операция без `inc`, корректировка операцией на разницу
|
|
205
|
+
под `forUpdate()`; «Дневная выручка» (§12) — проектор со свёрткой по дню из данных продажи и таблица «агрегат или
|
|
206
|
+
проектор». `describe()` отдаёт `derivedBy` и `aggregate`. MCP: ответы `letopis_model` (краткая модель с
|
|
207
|
+
`brief: true`, если полная больше `maxBytes`), `letopis_history` и `letopis_write`, концы записи `links`, предел
|
|
208
|
+
`maxBytes` — по JSON строк (строка больше предела приходит на странице одна), отказ фасаду `db.begin()`
|
|
209
|
+
(`invalid_query`), код выхода 2 у `letopis mcp` без токена и с `--api-key`. «Справочник API»:
|
|
210
|
+
типы проекторов, `AggregateInfo` и `AggregateDef`, опции `createMcpServer` с умолчаниями и пределами, флаги и
|
|
211
|
+
переменные `letopis mcp`, число исправленных строк у `rebuildAggregates`, подраздел «Проектор» (опции, методы,
|
|
212
|
+
`ProjectorReport`, `ProjectorStatus`, `ProjectorFailure`, `ProjectorChange`, `ProjectorOut`). «Ошибки»:
|
|
213
|
+
`admin_required` — и у `rebuildAggregates`, `user_session_required` — и без токена, `derived_class` — и у проектора
|
|
214
|
+
без участников. «Ограничения», «Проекторы»: только администраторское подключение и журнал всех арендаторов в обход
|
|
215
|
+
RLS, участник без опции `tenants` — любой арендатор с пометкой `derivedBy`, производные строки пишет только проектор,
|
|
216
|
+
ревизия движка. Сверка нашла и расхождение кода с README: источник с `history: false` проектор принимал молча —
|
|
217
|
+
исправлено (раздел «Security», Н1, продолжение). Шпаргалка (обе версии): `derivedBy` и `aggregate` в полной форме
|
|
218
|
+
`hub()`, свёртка дневного итога, право на описание агрегата, `LETOPIS_*`, `maxRows` и `maxBytes`, администраторское
|
|
219
|
+
подключение у проектора и `rebuildAggregates` (грабли №8).
|
|
220
|
+
- Замеры этапа 6 (план 1.1, §5, этап 6, п. 2): новый `lib/bench/derived.bench.mjs` — цена агрегата при записи (один
|
|
221
|
+
писатель, 20 писателей одного и разных кошельков, сырой SQL), догонка проектора на 100 тыс. изменений, перестройка
|
|
222
|
+
атомарная и пачками, задержка подписки во время перестройки, цена триггеров агрегатов у классов без агрегатов;
|
|
223
|
+
повтор `lib/bench/vs-101.bench.mjs` (четыре прогона — `lib/bench/results/vs-101-1.2.md`). Итог и выводы —
|
|
224
|
+
`docs/checkpoint-1.2.md`. По замерам README (обе версии): §17 — числа 1.2; горячий счётчик — около 150–190 операций
|
|
225
|
+
в секунду на объект, а не «тысячи записей» (§5, «Рецепт: баланс агрегатом», «Ограничения»); §12 — скорость
|
|
226
|
+
проектора задаёт число правок строк, а не изменений (свёртку `inc` в `out` добавила точка Б); в таблице «агрегат или
|
|
227
|
+
проектор» проектор применяет изменения пачками; «Ограничения» — цена атомарной перестройки
|
|
228
|
+
и очередь фиксаций с `NOTIFY` у писателей сырым SQL; §2 «Сигналы» — `db.sql` и `db.transaction()` шлют сигналы
|
|
229
|
+
отложенно, как другие транзакции библиотеки. Шпаргалка (обе версии) — горячий счётчик.
|
|
230
|
+
|
|
231
|
+
### Security — ревью безопасности №2
|
|
232
|
+
- Н1 (проекторы, отказ в обслуживании между арендаторами). Участником проектора становится любой арендатор, который
|
|
233
|
+
пометит свои классы его `derivedBy`, а пачка всех арендаторов была одной транзакцией и одним `derived_apply`: ошибка
|
|
234
|
+
данных одного (например, `DayTotal` с `maximum: 0`) откатывала пачку, курсор стоял, `rebuild()` падал, перестройка
|
|
235
|
+
пачками успевала стереть производные строки всех и оставалась в `rebuilding`, а `status()` молчал. Теперь
|
|
236
|
+
(`lib/src/project.ts`: `apply`, `atomic`, `chunked`) пачка делится по арендаторам: `handle` получает изменения одного
|
|
237
|
+
арендатора, запись его результатов идёт под точкой сохранения. Ошибка данных арендатора — отказ базы при записи его
|
|
238
|
+
результатов (коды letopis, неверные значения, пределы размера) или отказ `out` по его строке или приращению —
|
|
239
|
+
откатывает только его группу: арендатор выбывает — `meta.failed[арендатор] = { cursor, code, message }` пишется в
|
|
240
|
+
чекпоинт в той же транзакции — и пропускается до `rebuild({ tenant: id })`, а остальные записываются. `status()`
|
|
241
|
+
отдаёт `failed`. Производные строки выбывшего больше не меняются: выбывший в `run()` сохраняет их на момент до
|
|
242
|
+
пачки с ошибкой, а выбывший посреди перестройки остаётся без них (частично построенные удаляются). Новая опция
|
|
243
|
+
`rebuild({ tenant })` перестраивает одного арендатора одной транзакцией: удаляет его производные строки, строит их из
|
|
244
|
+
его текущих строк источников и снимает отметку; снимок перестройки встаёт в чекпоинт (`meta.resync`), и хвост
|
|
245
|
+
журнала не применяет учтённые изменения второй раз. Полная перестройка начинает всех участников заново. Текст ошибки
|
|
246
|
+
`derived_apply` называет проектор, арендатора и id строки, а `fromDbError` сохраняет у `ValidationError` текст базы
|
|
247
|
+
целиком. Опция `tenants` в `ProjectorOptions` — явный список участников. Исключение `handle`, нарушение границ `out`
|
|
248
|
+
и сбой окружения (взаимная блокировка, отмена запроса, обрыв связи) по-прежнему откатывают пачку целиком: это не
|
|
249
|
+
данные арендатора, и выводить его из проекции не за что. Тип `ProjectorFailure`.
|
|
250
|
+
- Н1, продолжение: ярус журнала и журнал источников — по арендатору. Тем же путём арендатор останавливал проектор
|
|
251
|
+
описанием своих классов: предел `cursor_expired` был общим — самый короткий ярус `all` политики хранения источников
|
|
252
|
+
всех участников, и ярус в одну секунду у классов одного арендатора ронял `run()` для всех (а при
|
|
253
|
+
`onExpired: 'rebuild'` зацикливал полные перестройки). Теперь ярус — по арендатору (`resolve`, `step`): курсор старше
|
|
254
|
+
яруса у всех участников — `cursor_expired`, как прежде; у части — выбывают они с кодом `cursor_expired`, а с
|
|
255
|
+
`onExpired: 'rebuild'` перестраиваются сами, каждый своей транзакцией (`rebuild({ tenant })`). Источник без журнала
|
|
256
|
+
(`history: false`) проектор принимал молча: изменений такого класса он не видит, и проекция арендатора расходилась с
|
|
257
|
+
данными. Теперь такой арендатор выбывает с кодом `invalid_class` (и при перестройке, атомарной и пачками), его
|
|
258
|
+
производных строк нет, а `rebuild({ tenant })` отказывает, пока журнал класса не включат.
|
|
259
|
+
- Н2 (известна с 1.0). Право WRITE на `Class` позволяло сменить у класса `parent` или `alias`, и его строки попадали
|
|
260
|
+
под правила READ, WRITE и DELETE нового предка или алиаса: план прав берёт для класса имена предков и их алиасы.
|
|
261
|
+
Теперь `class_admit` при правке, которая меняет `resolved.ancestors`, `alias` или `resolved.derivedBy`, зовёт функцию
|
|
262
|
+
владельца `class_acl_admit` (`lib/sql/60-write.sql`, по образцу `agg_source_admit`): для самого класса и каждого его
|
|
263
|
+
потомка своего арендатора `acl_check_data` по READ, WRITE и DELETE должна разрешать без условий, иначе `acl_denied`;
|
|
264
|
+
потомки класса System у арендаторов пропускаются, как у агрегатов. Проверка идёт после проверок агрегатов. Алиас не
|
|
265
|
+
совпадает с именем другого класса (в том числе `Class` и системных) и с чужим алиасом арендатора и System —
|
|
266
|
+
`invalid_class`, и у нового класса тоже. И наоборот: имя класса не совпадает с чужим алиасом арендатора и System
|
|
267
|
+
(автоматическое ревью коммита `6c66d02`): иначе правило, написанное для нового класса, действовало бы и на строки
|
|
268
|
+
класса с этим алиасом. `class_acl_admit` — не `security definer`: приложению она не выдана, словарь
|
|
269
|
+
негативных тестов `roles` №4 не изменился.
|
|
270
|
+
- О1. Чекпоинт проектора искался по виду `PROJECTOR` и имени, без аккаунта. Теперь `projector_state`,
|
|
271
|
+
`projector_advance` и `status()` берут только строку System. Накат помечает удалёнными строки `PROJECTOR` других
|
|
272
|
+
аккаунтов (идемпотентно), а `projector_state`, не найдя своей строки, так же помечает чужую с тем же именем: иначе
|
|
273
|
+
уникальный индекс `secret_kind_ident` не дал бы записать свою.
|
|
274
|
+
- О2. `derived_clear` удалял строки всех классов с пометкой проектора во всех арендаторах. Теперь
|
|
275
|
+
`derived_clear(имя, арендаторы[], классы[])` удаляет только пары «арендатор, класс», которые передаёт проектор
|
|
276
|
+
(участники и их семейства `into`), и только у классов с его пометкой; прежняя `derived_clear(text)` удаляется при
|
|
277
|
+
накате.
|
|
278
|
+
- О7 (MCP). Окно подтверждения удаления показывало алиас класса так, как его прислал агент, — текст арендатора. Теперь
|
|
279
|
+
в окне каноническое имя класса (`registry.resolve(cls).name`) и id целей (первые 10, дальше «и ещё K»); алиас и
|
|
280
|
+
`meta` в текст не попадают.
|
|
281
|
+
- О8 (MCP). `countBy` и `versions` читали всё и обрезали в памяти. Теперь предел — в самом запросе: `countBy` читает
|
|
282
|
+
самые частые `maxRows` + 1 групп, `versions` — последние `maxRows` + 1 версий. Для этого в цепочке `limit()` и
|
|
283
|
+
`offset()` перед `countBy()` относятся к группам (по убыванию числа), а `versions()` учитывает `sort()`
|
|
284
|
+
(`sort('at', 'desc').limit(n).versions()` — n последних версий). Ответ `countBy` — без `groups`, ответ `versions` —
|
|
285
|
+
без `total`. Шагов `steps` — не больше 16, условий `$or` в одном фильтре — не больше 64, иначе `invalid_query`.
|
|
286
|
+
README советует `statement_timeout` для роли базы сервера MCP: время запроса пределы не ограничивают.
|
|
287
|
+
- Смысл `before` у проектора. У первой версии нового id после `reclass` и `rekey` `before` была последняя версия
|
|
288
|
+
прежнего id, а у надгробия прежнего id — тоже она: свёртка вычитала её дважды (пример README §12 после `rekey` давал
|
|
289
|
+
0 вместо 10). Теперь `before` — всегда прошлая версия того же id (`fillBefore`): у первой версии нового id — `null`,
|
|
290
|
+
прежние данные вычитает надгробие прежнего id.
|
|
291
|
+
- Документация: README (обе версии) — §12 «Проекторы»: `failed` в `status()`, `rebuild({ tenant })`, опция `tenants`,
|
|
292
|
+
смысл `before`, обработка ошибки `run()` в примере (`catch`, а не `void`); §10 «Права» — смена `parent`, `alias` и
|
|
293
|
+
`derivedBy`; «Агенты и MCP» — форма ответа `countBy`, пределы шагов и `$or`, окно подтверждения; «Агрегаты» и
|
|
294
|
+
«Версии» — `limit()` у `countBy()` и `sort()` у `versions()`; §15 «Цепочка» — терминалы `diff()` и `blame()`.
|
|
295
|
+
Шпаргалка (обе версии) — то же коротко.
|
|
296
|
+
|
|
297
|
+
### Tests — ревью безопасности №2
|
|
298
|
+
- `projector`: арендатор x с `DayTotal` (`derivedBy: 'daily'`, у `total` — `maximum: 0`) и строки `Sale` у x и t —
|
|
299
|
+
`run({ once: true })` проходит, итог t записан, `status().failed` содержит x с `invalid_data` и текстом с арендатором
|
|
300
|
+
и id строки (и арендатора y, чью сумму-строку отклонил `out.inc`); `rebuild()` и `rebuild({ chunked: true })` дают
|
|
301
|
+
верные итоги остальных; пока описание не исправлено, `rebuild({ tenant })` отклоняется; после снятия ограничения
|
|
302
|
+
`rebuild({ tenant: x.id })` восстанавливает x, а хвост журнала применяет его изменения ровно один раз; опция `tenants`
|
|
303
|
+
отсекает x — его изменения и строки, не участник — `tenant_denied`. Арендатор z, выбывший посреди перестройки
|
|
304
|
+
(`batch: 1`: первая строка источника проходит, вторая — нет), после `rebuild()` и `rebuild({ chunked: true })`
|
|
305
|
+
остаётся без производных строк, а не с их частью. Ярус `all` источника одного арендатора (1 день) при курсоре
|
|
306
|
+
старше трёх дней — выбывает только он (`cursor_expired`), изменения остальных применены; `rebuild({ tenant })`
|
|
307
|
+
возвращает его, `onExpired: 'rebuild'` перестраивает его сам; ярус истёк у всех — `cursor_expired`, как прежде.
|
|
308
|
+
Источник с `history: false` — арендатор выбывает с `invalid_class` (и при перестройке, атомарной и пачками),
|
|
309
|
+
`rebuild({ tenant })` отказывает до включения журнала, после — возвращает его. Подложенная строка `PROJECTOR`
|
|
310
|
+
другого аккаунта не мешает проектору, накат её удаляет. Перестройка не трогает класс с пометкой вне семейства
|
|
311
|
+
`into`. `rekey` строки источника — итог свёртки верный, `before` первой версии нового id — `null`.
|
|
312
|
+
- `acl`: участник с одним правом WRITE на `Class` меняет у класса `alias`, затем `parent` — оба раза `acl_denied`,
|
|
313
|
+
`rev` строки `Class` не меняется; условного права READ и READ без WRITE и DELETE мало, запрет DELETE на потомка —
|
|
314
|
+
отказ; с правами без условий — проходит; алиас, совпадающий с именем класса, системным именем или чужим алиасом, —
|
|
315
|
+
`invalid_class`, у нового класса тоже. Подкласс класса System, чьи строки закрывает запрет System весом 1000, не
|
|
316
|
+
снять с наследования (`parent` убран, поля скопированы) — `acl_denied`, случай (г) ревью. Новый класс с именем,
|
|
317
|
+
совпадающим с алиасом класса арендатора или System, — `invalid_class`.
|
|
318
|
+
- `engine-attacks` №36: то же сырым SQL — `alias`, `parent` и `derivedBy` без прав на строки, алиас — имя класса.
|
|
319
|
+
- `mcp`: окно подтверждения удаления по алиасу — имя `Task`, первые 10 id и «и ещё 2», без алиаса и `meta`;
|
|
320
|
+
`countBy` по 1000 группам и `versions` по 150 версиям читают из базы не больше 101 строки (`onQuery`); 17 шагов и
|
|
321
|
+
65 условий `$or` — `invalid_query`, 16 и 64 — можно.
|
|
322
|
+
- `aggregate`: правка описания без изменения агрегата — `meta` вместо `alias` (смена алиаса теперь требует прав на
|
|
323
|
+
строки класса); перенос `Secret` под `Sale` — ещё и с правами WRITE и DELETE на его строки.
|
|
324
|
+
|
|
325
|
+
### Changed — точка Б (решения владельца)
|
|
326
|
+
- Перестройка проектора в автоматических путях — пачками: первый `run()` новой проекции и `onExpired: 'rebuild'`
|
|
327
|
+
перестраивают пачками, и горизонт подписок кластера стоит не дольше пачки; `rebuild()` без опций остаётся
|
|
328
|
+
атомарным (`lib/src/project.ts`: `run`, `rebuildNow`, `chunked`). Второй процесс того же проектора во время такой
|
|
329
|
+
перестройки не падает с `projector_busy`: занятую блокировку он пережидает, а начатую другим процессом перестройку
|
|
330
|
+
продолжает, а не начинает заново. Перестройка одного арендатора по `onExpired: 'rebuild'` по-прежнему идёт одной
|
|
331
|
+
транзакцией.
|
|
332
|
+
- Свёртка `out.inc` (предложение по замерам этапа 6): приращения строки, заданной одними полями ключа или одним `$id`,
|
|
333
|
+
`out` складывает до отправки в базу, пока между ними нет `upsert`, `delete` или `get`, — одна правка строки за пачку
|
|
334
|
+
(`Out`, `Projector.foldKey`; поля ключа классов `into` проектор берёт из их описаний). Правило класса проверяется над
|
|
335
|
+
итогом приращений строки в пачке, а не над каждым промежуточным значением; строку с другими полями `inc` не
|
|
336
|
+
складывает. Повтор серий 2 и 3 бенча (`BENCH_SERIES=2,3` — новый выбор серий, итог —
|
|
337
|
+
`lib/bench/results/derived-23.md`): handle с `out.inc` на каждое изменение догоняет 6 076 и 33 395 изменений в
|
|
338
|
+
секунду (`batch` 500 и 2000) вместо 992 и 1 203, атомарная перестройка 75 тыс. строк — 13,3–13,5 с вместо 101 с,
|
|
339
|
+
перестройка пачками — 13,0–13,2 с вместо 58–63 с.
|
|
340
|
+
- О5 ревью безопасности №2 — отказывать по умолчанию: `letopis_write` не пишет, не правит и не удаляет строки классов
|
|
341
|
+
прав — `rule`, `Resource`, `member`, `Account`, `Class` и их потомков (`acl_denied`), а `createMcpServer` отклоняет
|
|
342
|
+
сессию в арендаторе System (`user_session_required`): так работает системный администратор, и его роль `admin`
|
|
343
|
+
действует только там. Разрешают это только новые опции `McpOptions` — `allowAclWrite` и `allowSystemAdmin`; у
|
|
344
|
+
`letopis mcp` таких флагов нет. Об отказе говорят описание `letopis_write` и `instructions` сервера.
|
|
345
|
+
- Документация: README и шпаргалка (обе версии) — §12 (перестройка `run()` пачками, свёртка `inc`, рецепт), «Агенты и
|
|
346
|
+
MCP», «Справочник API», «Ошибки», «Ограничения», §17; `docs/checkpoint-1.2.md` — решения владельца и повтор замера;
|
|
347
|
+
`reports/PLAN-LETOPIS-1.1.md` — решения точки Б в шапке.
|
|
348
|
+
|
|
349
|
+
### Tests — точка Б
|
|
350
|
+
- `projector`: три приращения строки в пачке — одна правка (`created` 1, `updated` 0); −5 и +5 по новой строке с
|
|
351
|
+
`minimum: 0` — итог 0, а не выбывание; `delete` и `get` свёртку сбрасывают; строка с полем вне ключа не складывается,
|
|
352
|
+
и порядок правок сохраняется. Первый `run()` новой проекции снаружи виден как `rebuilding`; два процесса одного
|
|
353
|
+
проектора на новой проекции (`batch: 1`) — ни один `run()` не падает, перестройка одна, итог верный.
|
|
354
|
+
- `mcp`: `create` классов `rule`, `Resource`, `member`, `Account`, `Class`, правка и удаление правила — `acl_denied`, об
|
|
355
|
+
этом говорит описание `letopis_write`; с `allowAclWrite` — превью удаления правила; сессия системного
|
|
356
|
+
администратора — `user_session_required`, с `allowSystemAdmin` — сервер работает; `letopis mcp` с её токеном — код 1;
|
|
357
|
+
неверный тип опций — `invalid_data`.
|
|
358
|
+
- `engine-attacks` №37: агент через MCP выдаёт постороннему членство с ролью `admin`, правило, ресурс и класс —
|
|
359
|
+
`acl_denied`, строк классов прав не прибавилось; сессия системного администратора — отказ.
|
|
360
|
+
- Без исправлений все новые тесты падают: каждое исправление отключалось по отдельности.
|
|
361
|
+
|
|
362
|
+
### Security — доработки после точки Б
|
|
363
|
+
- Производный класс — только у администратора (`/security-review` после точки Б). Пользователь арендатора с правом
|
|
364
|
+
WRITE на `Class` (по умолчанию — любой) заводил класс с `derivedBy` проектора, проектор без `tenants` брал его
|
|
365
|
+
арендатора в участники и писал туда итоги по всем строкам источников, в том числе по тем, которые пользователю читать
|
|
366
|
+
запрещено. Теперь `class_admit` (`lib/sql/60-write.sql`) пропускает вставку и правку строки `Class` производного
|
|
367
|
+
класса — свой `derivedBy` или унаследованный, до правки или после — и `trg_entity_ad` её удаление только с правом
|
|
368
|
+
`letopis.projector` (`letopis_allowed`): оно есть у администраторской роли подключения и по правилам System —
|
|
369
|
+
у системного администратора по маске `letopis.*`; иначе `acl_denied`. Правила сида не меняются. Производные классы
|
|
370
|
+
синхронизирует администраторское подключение с сессией арендатора; `letopis sync` принимает такое подключение
|
|
371
|
+
(`allowBypassRls`, как `load`, `verify` и `maintain`). README и шпаргалка: кто заводит производные классы, рецепт
|
|
372
|
+
дневной выручки, «Ограничения» — итоги производного класса видит каждый, кому правила дают на него READ, а с
|
|
373
|
+
`history: true` его журнал показывает изменение каждой пачки.
|
|
374
|
+
- `rekeyClass` предка не переносит строки производного потомка (укрепление из ревью №2). Производный потомок
|
|
375
|
+
непроизводного предка наследует его ключ, а `rekey_class` (`lib/sql/90-time.sql`) проверял `derivedBy` только у
|
|
376
|
+
самого класса и переносил строки потомка на новые id мимо проектора. Теперь класс с производным потомком —
|
|
377
|
+
`derived_class`.
|
|
378
|
+
- MCP: `structuredContent` с пояснением (укрепление из ревью №2). Данные базы в текстовом дубле стоят внутри
|
|
379
|
+
`<untrusted-data-…>`, а `structuredContent` повторял их без пояснения — клиент, который отдаёт модели его, обходил
|
|
380
|
+
смягчение. Теперь `structuredContent` — `{ notice, data }` у ответа и `{ notice, error }` у отказа (`ok` и
|
|
381
|
+
`failure` в `lib/src/mcp.ts`): данные базы — только в `data` и `error`, `notice` — постоянный текст пакета. Прежние
|
|
382
|
+
формы ответов — внутри `data`. README и шпаргалка (обе версии): «Агенты и MCP», «Ограничения».
|
|
383
|
+
- Удаление и пересоздание класса (остаток Н2 ревью №2). Пустой класс можно было удалить и создать заново с другим
|
|
384
|
+
`parent` или `alias`: история его удалённых строк осталась в журнале, а правила прав находят строки по именам
|
|
385
|
+
класса, предков и алиасов — старые версии попадали под правила новых имён. Теперь `trg_entity_ad` не удаляет класс,
|
|
386
|
+
пока в журнале есть версии его строк (кроме записей `purge`, `trim`, `reset`; у класса System — у любого арендатора),
|
|
387
|
+
— `invalid_class`, историю удалённых строк сначала стирает `purge()`; а `trg_entity_bi` создаёт заново класс,
|
|
388
|
+
удалённый с историей раньше (надгробие строки `Class` хранит прежнее описание), только с прежними
|
|
389
|
+
`resolved.ancestors`, `alias` и `resolved.derivedBy` — иначе `invalid_class`. Если у истории строк нет надгробия
|
|
390
|
+
своего класса (строки класса System, удалённого в 1.1, у арендатора), класс с этим именем не создаётся вовсе, пока
|
|
391
|
+
историю не сотрёт `purge()` (автоматическое ревью коммита). Функция владельца `class_has_history`.
|
|
392
|
+
- Ужесточение класса — не оракул по невидимым строкам (ревью №2, вне его области: было до 1.2). Пробное ужесточение
|
|
393
|
+
(`maximum`, `enum`, `pattern` и т. п.) отвечало `tightening_conflict` с числом невидимых актору строк, которые ему не
|
|
394
|
+
соответствуют (`others`), и двоичным поиском узнавались значения чужих строк. Теперь ужесточение (`class_evolve`) и
|
|
395
|
+
смена ключа (`rekey_class`) до проверки строк требуют права READ без условий на все строки класса и его потомков
|
|
396
|
+
своего арендатора (`class_rows_admit`, функция владельца; через неё теперь и `class_acl_admit`), иначе `acl_denied`
|
|
397
|
+
без деталей. Описание агрегата — тоже ужесточение цели. Число строк других арендаторов при ужесточении класса System
|
|
398
|
+
системным администратором остаётся: это уровень установки. В тексте `problem` id ссылающейся строки — только видимой.
|
|
399
|
+
- Кэш реестра `db.as()` (ревью №2, вне его области: было до 1.2). Реестр арендатора, общий для всех `db.as()`,
|
|
400
|
+
перечитывался под контекстом первого вызвавшего: если тот выбыл из арендатора или его сессия кончилась, ошибка
|
|
401
|
+
перечитывания опустошала реестр для всех пользователей арендатора (`load([])`), два одновременных `as()` нового
|
|
402
|
+
арендатора создавали два реестра (первый больше не обновлялся), а каждый сигнал `Class` перечитывал реестры всех
|
|
403
|
+
арендаторов процесса. Теперь (`lib/src/index.ts`: `registryFor`, `refresh`, `reload`) перечитывание идёт под
|
|
404
|
+
контекстом последнего успешного `as()` арендатора, ошибка или контекст не в этом арендаторе оставляют прежний
|
|
405
|
+
реестр, одновременные `as()` получают один реестр, а сигнал `Class` с токеном арендатора (режимы `tenant` и
|
|
406
|
+
`opaque`) перечитывает только его реестр.
|
|
407
|
+
- Политика хранения истории (ревью №2, вне его области: было до 1.2). Одно право WRITE на `Class` задавало
|
|
408
|
+
`history: { all: '1 day' }` или `false`, и ближайший `maintain()` стирал аудит строк, на которые у автора правки
|
|
409
|
+
прав нет (`trimHistory` требует DELETE и READ на каждую цель). Теперь `class_admit` пропускает смену развёрнутой
|
|
410
|
+
`history` существующего класса только с правом READ и DELETE без условий на все строки класса и его потомков своего
|
|
411
|
+
арендатора (`class_rows_admit`), иначе `acl_denied`.
|
|
412
|
+
- `reclass` (ревью №2, вне его области: было до 1.2). Смена класса проверяла WRITE на прежнюю и новую строку, но не
|
|
413
|
+
DELETE: с одним WRITE строку переносили в класс, где есть DELETE, и удаляли там. Перевод ссылок (`move_row`)
|
|
414
|
+
переписывал строки глубже первого уровня (ссылающиеся на строку, чей id сменился вместе с ключом) без проверки
|
|
415
|
+
прав, а текст отказа называл класс невидимой ссылающейся строки. Теперь `reclass` и сырая смена класса
|
|
416
|
+
(`trg_entity_bu`) требуют права DELETE на прежнюю строку; `move_row` получает план WRITE актора и проверяет каждую
|
|
417
|
+
переписываемую строку на всех уровнях (`reclass`, `rekey`, `rekey_class`; пути владельца — без плана); класс
|
|
418
|
+
неправимой ссылающейся строки называется, только если актор её видит. Число невидимых мешающих строк в
|
|
419
|
+
`reclass_denied` остаётся, как у `restrict` (решение плана 1.0).
|
|
420
|
+
- Цена чтения не растёт с данными других арендаторов (ревью №2, вне его области: было до 1.2). `find_ids`
|
|
421
|
+
(`lib/sql/70-read.sql`) не называла арендатора: строки и версии классов с тем же именем у всех арендаторов
|
|
422
|
+
перебирались, и только `row_visible` отсекала чужие, а срез `asOf` и `withDeleted` по классу шёл по индексу
|
|
423
|
+
`log_class_at (class, at)` без арендатора. Теперь ветви по классу, вложению и концам берут строки арендатора сессии и
|
|
424
|
+
System явным условием, а свои членства в других арендаторах — отдельным запросом по концу `User`: это ровно то, что
|
|
425
|
+
пропускает `row_visible`. Индекс журнала — `log_class_tenant_at (class, tenant, at)` вместо `log_class_at`; первый
|
|
426
|
+
`up()` после обновления строит его, запись в журнал на это время ждёт (README, «Обновление пакета и схемы»: как
|
|
427
|
+
построить его заранее `create index concurrently`). Ревизия движка остаётся 6.
|
|
428
|
+
- MCP: подпись подтверждения удаления привязана к пользователю и истекает (укрепление из ревью №2). В ревизии
|
|
429
|
+
2026-07-28 `requestState` был HMAC одного хэша операции ключом процесса: подпись, выданная серверу одного
|
|
430
|
+
пользователя, проходила у сервера другого пользователя того же арендатора и процесса (у той же операции тот же хэш)
|
|
431
|
+
и не истекала. Теперь `requestState` — срок и HMAC аккаунта и арендатора сессии сервера, срока и хэша операции
|
|
432
|
+
(`stateOf`, `stateError` в `lib/src/mcp.ts`); срок — 10 минут, сравнение — `timingSafeEqual`. Подделанная подпись,
|
|
433
|
+
подпись другого пользователя и устаревшая — `confirmation_required`.
|
|
434
|
+
|
|
435
|
+
### Changed — доработки после точки Б
|
|
436
|
+
- Проекторы: перестройка одного арендатора пачками и повтор автоматической перестройки (`lib/src/project.ts`).
|
|
437
|
+
`rebuild({ tenant, chunked: true })` перестраивает арендатора пачками — каждая своей транзакцией со своим снимком,
|
|
438
|
+
пока хвост журнала идёт для всех: изменения арендатора выше последней пачки хвост пропускает (их прочтёт пачка), в
|
|
439
|
+
пройденной пачке — если транзакция видна в её снимке; карта пачек — `meta.retenant` чекпоинта. `onExpired: 'rebuild'`
|
|
440
|
+
перестраивает выбывшего по ярусу пачками, а не одной транзакцией; состояние лежит в чекпоинте, и перестройку
|
|
441
|
+
продолжает `run()` любого процесса. Прежде неудавшаяся перестройка (занятая блокировка, ошибка) молча оставляла
|
|
442
|
+
арендатора выбывшим навсегда, а выбывшего в другом процессе не перестраивал никто. Теперь занятая блокировка — повтор
|
|
443
|
+
на следующем проходе, ошибка данных — выбывание с её кодом без повторов. Ярус перестроенного арендатора
|
|
444
|
+
отсчитывается от начала его перестройки (`meta.since`), пока курсор её не догнал: прежде отстающий проектор выводил
|
|
445
|
+
его по `cursor_expired` сразу после перестройки. `status().rebuildingTenants` — кого перестраивают пачками.
|
|
446
|
+
Арендатор, который к автоматической перестройке перестал подходить (не участник, источник без журнала), выбывает
|
|
447
|
+
с причиной и не останавливает `run()` для всех (автоматическое ревью коммита).
|
|
448
|
+
|
|
449
|
+
- Подписка (хвосты выпуска 1.1.0, `lib/src/watch.ts`). `sub.ready()` — обещание начальной точки: у `from: 'now'` —
|
|
450
|
+
горизонт и снимок момента подписки (`watch_head()`), у `'start'` и курсора — сразу; запись после него в поток
|
|
451
|
+
придёт. `next()` ставит вызовы в очередь: параллельные вызовы получают разные события — прежде два вызова читали
|
|
452
|
+
журнал с одной позиции и отдавали событие дважды, а второй затирал ожидание первого. Тестовый помощник
|
|
453
|
+
`watchStarted` (читал приватное поле) снят: тесты ждут `sub.ready()`.
|
|
454
|
+
|
|
455
|
+
- Первая заливка (хвост выпуска 1.1.0): `esc` без замен для значений без обратной косой, табуляции и переводов строки,
|
|
456
|
+
голова строк `entity` и `log` собирается один раз, `uuidv5` — `crypto.hash` и кэш байтов пространства имён (байты
|
|
457
|
+
`COPY` прежние). Бенч `initial-load.mjs` — выбор прогонов `BENCH_RUNS`. Замер на полном полигоне: первая заливка
|
|
458
|
+
78,5 с, обычный загрузчик 233,3 с — 2,97×; предел первой заливки — запись `COPY` на сервере (генерируемые колонки
|
|
459
|
+
`ends` и `hash`, WAL), поэтому подготовку в `worker_threads` по решению владельца не делали
|
|
460
|
+
(`docs/checkpoint-1.2.md`, «Доработки после точки Б: первая заливка»).
|
|
461
|
+
|
|
462
|
+
- `check-docs`: ссылки, якоря и управляющие символы сверяются во всех `docs/*.md` (прежде из отчётов — только
|
|
463
|
+
`docs/checkpoint-a.md`); новая проверка `paths` — пути в обратных кавычках в `docs/checkpoint-*.md` (`lib/…`,
|
|
464
|
+
`docs/…`, `reports/…`, `.github/…`) существуют.
|
|
465
|
+
|
|
466
|
+
- Ревью безопасности №3, №1: пустой последний проход перестройки пачками (полной и одного арендатора) теперь тоже
|
|
467
|
+
пачка со своим снимком — до конца диапазона id. Прежде хвост применял изменения id выше последней прочитанной
|
|
468
|
+
строки, уже учтённые этим проходом: надгробие строки с наибольшим id, удалённой во время перестройки, вычиталось
|
|
469
|
+
лишний раз.
|
|
470
|
+
|
|
471
|
+
- Ревью безопасности №3, №6: арендатор, которого хвост выводит посреди его перестройки пачками (ошибка данных,
|
|
472
|
+
источник без журнала), остаётся без производных строк — частично построенные удаляются, как обещает README.
|
|
473
|
+
|
|
474
|
+
- Ревью безопасности №3, №3: перенос строки (`move_row`: `reclass`, `rekey`, `rekeyClass`) не переписывает ссылки
|
|
475
|
+
строк производного класса, которые на неё ссылаются, — `derived_class`: их пишет только проектор (прежде перевод
|
|
476
|
+
ссылок шёл мимо него, и проекция расходилась).
|
|
477
|
+
|
|
478
|
+
- `/security-review` после ревью безопасности №3: имя строки-замка лежит в `meta`, а не в `ident`. Уникальный индекс
|
|
479
|
+
`ident` таблицы `secret` сравнивает без регистра, поэтому строка-замок семейства, чей корень отличается от корня
|
|
480
|
+
другого семейства того же арендатора только регистром (`Kase` и `kase`), не вставлялась, и его блокировка молча не
|
|
481
|
+
бралась: ужесточение шло мимо незафиксированной записи. Так же отключала блокировку строка вида `LOCK`, заведённая в
|
|
482
|
+
1.1 функциями кредов (тогда этот вид был им открыт), под именем блокировки: накат теперь помечает такие строки
|
|
483
|
+
удалёнными. Если строки-замка нет и после вставки, `family_lock` отвечает ошибкой, а не пишет без блокировки.
|
|
484
|
+
|
|
485
|
+
- Ревью безопасности №3, №5: `purge()` удалённого объекта отказывает новым кодом `projector_pending` (LT023), пока
|
|
486
|
+
курсор проектора, чьи источники `from` включают класс объекта, а у арендатора объекта есть его производный класс,
|
|
487
|
+
не прошёл надгробие (в том числе надгробие этой же транзакции). Прежде `purge` стирал непрочитанное надгробие, и
|
|
488
|
+
вклад объекта навсегда оставался в итогах. Чекпоинт проектора хранит `from` (`meta.from`; чекпоинт без него касается
|
|
489
|
+
всех классов). Брошенный проектор, чьи производные классы удалены, `purge` не держит.
|
|
490
|
+
|
|
491
|
+
- Ревью безопасности №3, №4: смена класса без смены id (классы без ключа: сырой `UPDATE … SET class`, `reclass()`)
|
|
492
|
+
журнал пишет версией уже нового класса, и подписка проектора по классам `from` ухода строки из семейства не видела —
|
|
493
|
+
её вклад оставался в итогах; вошедшая так строка приходила с `before` — версией чужого класса, и свёртка вычитала
|
|
494
|
+
её. Теперь уход приходит удалением (`op` `delete`, `row` — последняя версия в семействе с `$deleted`), а у
|
|
495
|
+
вошедшей `before` = `null`. `watch_read(…, p_leaving)` отдаёт такие версии проектору; подписке `db.watch()` — нет.
|
|
496
|
+
|
|
497
|
+
- Ревью безопасности №3, №2 (было до 1.2): внутренние блокировки — семейства классов (`family_lock`), проектора и
|
|
498
|
+
обслуживания — больше не advisory, а строки-замки: строки вида `LOCK` таблицы `secret`, которой у приложения нет
|
|
499
|
+
(`lock_row`, `lock_try`). Advisory-блокировку берёт любая роль базы, а её ключ виден всем в `pg_locks`: посторонняя
|
|
500
|
+
роль занимала блокировку с ключом из `hashtext` открытого имени (или подсмотренным) и останавливала запись
|
|
501
|
+
семейства, проектор или обслуживание для всех. Писатели берут строку-замок семейства `for key share`, ужесточение и
|
|
502
|
+
`rekeyClass` — `for update`. Строковые блокировки очереди не держат — поток записей не дал бы ужесточению случиться
|
|
503
|
+
никогда, — поэтому ужесточение сначала занимает шлюз семейства (вторую строку-замок), и новые писатели ждут у него.
|
|
504
|
+
Писатель проходит шлюз в подтранзакции один раз за транзакцию: справка о взятых блокировках
|
|
505
|
+
(`letopis.family_locks`) пропускает только шлюз, а строку-замок функция берёт на каждую строку — повторно взятая
|
|
506
|
+
своя блокировка не ждёт. Справку клиент выставляет сам, и в первой редакции, где по ней пропускалась и блокировка,
|
|
507
|
+
запись с подделанной справкой шла мимо ужесточения (автоматическое ревью коммита 1bc6584). Строки-замки заводятся
|
|
508
|
+
заранее — при записи строки `Class` и в `seed_system()` (установка, `up()` после обновления, `reset({ all })`).
|
|
509
|
+
Блокировку обслуживания берёт сама функция `maintain()`. Цена — строковая блокировка на строку записи и
|
|
510
|
+
подтранзакция на первую запись в семейство за транзакцию; замер (`bench/family-lock.bench.mjs`, медиана трёх
|
|
511
|
+
кругов) — в пределах шума против advisory 1.1: построчная запись 159 и 157 строк/с, 20 писателей 576
|
|
512
|
+
и 544 строк/с, 1000 строк одним оператором 303 и 300 мс (`bench/results/family-lock.md`). id строки-замка —
|
|
513
|
+
`lock_id`: первые 16 байт `sha256` имени (с `v5_id` вставка 1000 строк одним оператором стоила 352 мс против 321).
|
|
514
|
+
|
|
515
|
+
### Tests — доработки после точки Б
|
|
516
|
+
- `engine-attacks` №38: сессия приложения заводит класс с `derivedBy`, потомка производного класса, правит
|
|
517
|
+
(`history`, снятие `derivedBy`) и удаляет производный класс — `acl_denied`, строк не прибавилось;
|
|
518
|
+
администраторское подключение с сессией арендатора (сырой SQL и `syncModels`) и системный администратор в System —
|
|
519
|
+
проходят. Производные классы тестов заводит `defineDerived` (`lib/test/_engine.ts`, администраторский пул с
|
|
520
|
+
токеном арендатора), правит — `asAdmin`; бенч `derived.bench.mjs` — так же.
|
|
521
|
+
- `engine-attacks` №39: `rekey_class` непроизводного предка с производным потомком — `derived_class`, строки потомка
|
|
522
|
+
и ключ предка прежние.
|
|
523
|
+
- `mcp` №5: у ответа `structuredContent` — ровно `notice` и `data`, у отказа — `notice` и `error`; текст-инструкция из
|
|
524
|
+
базы и из запроса — только внутри `data` и `error`. Остальные проверки `mcp`, `engine-attacks` №35, №37 и `docs`
|
|
525
|
+
читают ответ из `data`.
|
|
526
|
+
- `engine-attacks` №40: класс Master, закрытый пользователю, с историей удалённых строк — удаление класса
|
|
527
|
+
`invalid_class`; удалённый раньше (надгробие) — создать его потомком `Note` или с алиасом нельзя, с прежним
|
|
528
|
+
описанием можно, история пользователю по-прежнему не видна; история без надгробия своего класса — класс с этим
|
|
529
|
+
именем не создать. `engine-class`: класс с историей строк удаляется только
|
|
530
|
+
после `purge()`, класс с `history: false` — сразу.
|
|
531
|
+
- `engine-attacks` №41: условное право READ на `Org` — ужесточение и `rekey_class` отклонены `acl_denied` без строк и
|
|
532
|
+
их числа; READ без условий — `tightening_conflict` с перечнем и `others: 0`. `aggregate`: описанию агрегата нужно ещё
|
|
533
|
+
право READ на строки цели.
|
|
534
|
+
- `engine-attacks` №42: `reclass` и сырая смена класса без права DELETE на прежнюю строку — отказ; `reclass`, чей
|
|
535
|
+
перевод ссылок переписал бы невидимую неправимую строку третьего уровня, — `acl_denied`, имени её класса в тексте
|
|
536
|
+
нет, строка не переписана.
|
|
537
|
+
- `engine-attacks` №43: пользователь с WRITE на `Class` и без DELETE на строки `Note` задаёт `history: { all: '1 day' }`
|
|
538
|
+
и `false` — `acl_denied`, описание прежнее; с правом DELETE — проходит.
|
|
539
|
+
- `impersonation`: одновременные `as()` — один реестр; пользователь последнего `as()` выбыл из арендатора — реестр не
|
|
540
|
+
пустеет, после `as()` другого пользователя перечитывается; в режиме `tenant` сигнал `Class` арендатора А не
|
|
541
|
+
перечитывает реестр арендатора Б.
|
|
542
|
+
- `projector`: `rebuild({ tenant, chunked: true })` по две строки, пока пишутся источники и хвост идёт в другом
|
|
543
|
+
процессе, — итоги арендатора и остальных равны сумме источников; выбывший по ярусу в процессе без `onExpired`
|
|
544
|
+
перестраивается процессом с `onExpired: 'rebuild'`; отстающий курсор после перестройки не выводит его снова; ошибка
|
|
545
|
+
данных при автоматической перестройке — выбывание с `invalid_data` и ни одной повторной попытки.
|
|
546
|
+
- `watch`: `ready()` у `'start'` и `'now'`; два параллельных `next()` — два разных события; ожидающие вызовы при
|
|
547
|
+
`close()` — `done`.
|
|
548
|
+
- `find-ids-parity`: свои членства в других арендаторах (живое и удалённое) и строки `Class` System — сейчас, `asOf`
|
|
549
|
+
и `withDeleted`: ответы `find_*` и прямого чтения под RLS совпадают. `explain`: срез `asOf` по классу у арендатора с
|
|
550
|
+
двадцатью версиями `Staff` читает из индекса `log_class_tenant_at` не больше 40 записей при пяти тысячах версий
|
|
551
|
+
`Staff` у другого арендатора (`pg_stat_get_xact_tuples_returned`); прежнего индекса нет.
|
|
552
|
+
- `mcp`: клиент ревизии 2026-07-28 повторяет вызов с подменённым `requestState` — подделанная подпись, подпись сервера
|
|
553
|
+
другого пользователя того же арендатора и истёкшая (`mock.timers` для `Date`) — `confirmation_required`, объект цел;
|
|
554
|
+
своя подпись до срока — удаление проходит.
|
|
555
|
+
- `projector` (ревью безопасности №3, №1 и №6): после начала перестройки арендатора пачками удалена строка с
|
|
556
|
+
наибольшим id, пачки её уже не прочтут — итог равен сумме живых строк; источник арендатора выключил журнал посреди
|
|
557
|
+
его перестройки пачками — арендатор выбывает (`invalid_class`) без частичных строк.
|
|
558
|
+
- `engine-attacks` №44 (ревью безопасности №3, №3): `reclass` строки, на которую ссылается строка производного
|
|
559
|
+
класса, — `derived_class`, ссылка и класс прежние.
|
|
560
|
+
- `projector` (ревью безопасности №3, №4): сырая смена класса строки без ключа из `Mv` в `Away` — изменение `delete`
|
|
561
|
+
с `$deleted`, классом `Mv` и прежними данными, вклад вычтен; `reclass()` обратно с новыми данными — изменение
|
|
562
|
+
`reclass` с `before` = `null`, итог верен.
|
|
563
|
+
- `engine-attacks` №45 (ревью безопасности №3, №2): пока идёт запись семейства, в `pg_locks` нет advisory-блокировок;
|
|
564
|
+
`secret` вида `LOCK`, `lock_try` и `lock_row` прикладной роли недоступны (`42501`); пока она держит advisory с
|
|
565
|
+
прежними ключами семейства, обслуживания и проектора, проходят запись, ужесточение, `maintain()` и `run()`
|
|
566
|
+
проектора. `engine-races` №3а: ужесточение во время непрерывной перекрывающейся записи в семейство проходит меньше
|
|
567
|
+
чем за 3 с (без шлюза ждало, пока писатели не остановятся). `maintain`: блокировку обслуживания держит транзакция
|
|
568
|
+
другого процесса (`lock_try`) — `skipped`. Бенч `bench/family-lock.bench.mjs` — цена блокировки семейства: строка-
|
|
569
|
+
замок со шлюзом против строки без шлюза и advisory 1.1.
|
|
570
|
+
- `projector` (ревью безопасности №3, №5): удаление, которого проектор ещё не прочёл, — `purge` отклонён
|
|
571
|
+
`projector_pending`; после `run()` — проходит, итог верен; удаление и `purge` одной транзакцией — отказ; класс вне
|
|
572
|
+
источников и арендатор без производного класса проектора — `purge` сразу. 7.5 №9: `purge` источника до `run()`
|
|
573
|
+
проекторов — `projector_pending`, после — служебное изменение `purge`.
|
|
574
|
+
- `engine-races` №3б и №3в (`/security-review` после ревью №3): корни семейств `Kase` и `kase` одного арендатора —
|
|
575
|
+
ужесточение `kase` ждёт незафиксированную запись в него и находит её строку; строка вида `LOCK` чужого аккаунта с
|
|
576
|
+
именем блокировки ещё не созданного семейства — блокировка семейства работает, накат помечает строку удалённой. Без
|
|
577
|
+
исправления ужесточение шло, не дожидаясь записи.
|
|
578
|
+
- `engine-attacks` №46 (автоматическое ревью коммита 1bc6584): транзакция выставляет справку `letopis.family_locks`
|
|
579
|
+
«семейство уже взято» и пишет строку, пока ужесточение держит семейство, — запись ждёт его и получает
|
|
580
|
+
`invalid_data`, строк, нарушающих новое описание, нет. Без исправления запись проходила сразу.
|
|
581
|
+
|
|
5
582
|
## [1.1.0] — 2026-10-11
|
|
6
583
|
|
|
7
584
|
letopis 1.1 по плану `reports/PLAN-LETOPIS-1.1.md` (редакция 5, этапы 0–3 и контрольная точка А): `diff` и `blame`
|