letopis 0.13.0 → 0.18.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/CHANGELOG.md CHANGED
@@ -2,6 +2,206 @@
2
2
 
3
3
  Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver.
4
4
 
5
+ ## [0.18.0] — 2026-07-13
6
+
7
+ ### Added — генератор id берёт default поля из Schema
8
+
9
+ - **`v5Id` подставляет default необязательного from-поля** (`lib/src/write.ts`): если
10
+ поле из `attributes.id.from` не передано в `create`, id считается из его `default`
11
+ (объект-правило `{default}` или DSL `'…|default:X'`). Раньше отсутствие from-поля
12
+ роняло `needs scalar data field`. Теперь необязательное поле участвует в
13
+ детерминированном id прозрачно — единственная правка ядра библиотеки в этом релизе.
14
+
15
+ ### Changed — цена каталога: отдельный класс `цена` (варианты + мультивалюта)
16
+
17
+ Демо-домен booking: у `Элемент` (Услуга/Товар/Комплекс) убрано скалярное поле `price`;
18
+ цена вынесена в новый LINK-класс.
19
+
20
+ - **`price·цена` (LINK)** `[ {Услуга|Товар|Комплекс} ]`: `note` (string, default
21
+ `'базовая'`) + `amounts` (record<валюта, число>, валюта — enum RUB/USD/EUR/AED).
22
+ `id = v5(элемент, note)`. У элемента несколько вариантов цены «по пожеланиям
23
+ клиента» (базовая / с дизайном / …), каждый — своя строка.
24
+ - **Зачем не поле-массив:** массив в `data` при `update` заменяется целиком (точечно
25
+ не поправить, гонки теряют правки, по цене не искать). Отдельные строки-варианты +
26
+ record внутри дают точечную правку одной валюты (record мержится по ключам), поиск
27
+ (`db.цена({amounts:{RUB:lte(2000)}}).Услуга()`), историю и версии каждого варианта.
28
+ - **v5-default в деле:** `db.Услуга(s).цена().create({amounts:{RUB:1500}})` без `note`
29
+ → `note='базовая'`, id детерминирован; повтор без note → та же строка (идемпотентно),
30
+ `amounts` домерживается точечно.
31
+ - Тесты/бенчи/полигоны/доки переведены на класс `цена`; `idgen.test.ts` — новый кейс
32
+ «v5 берёт default из Schema».
33
+ - **Единый демо-полигон `v1.salondemo`** (`bench/salon-seed.mjs`): тесты быстродействия,
34
+ API Reference (`bench/api-reference-demo.mjs`) и статья (`bench/salon-article-demo.mjs`,
35
+ `bench/article-demo.mjs`) читают ОДИН полигон (~980k строк), мутируя лишь свои демо-сущности
36
+ (непересекающиеся префиксы `00000900`/`00000000-09xx`/`00000902`); `v1.article` упразднён.
37
+
38
+ ### Fixed — keyset-пагинация стабильна при неуникальном sort-поле
39
+
40
+ - **`orderExpr` добавляет `id` вторым ключом `ORDER BY`** (`lib/src/sql.ts`) тем же
41
+ направлением, что кортеж `(sortExpr, id)` в `.after()`. Раньше внешний `ORDER BY`
42
+ сортировал только по sort-полю: при неуникальных значениях (тысячи записей с одним
43
+ `updated`) страницы keyset перекрывались. Теперь `.sort()+.after()` даёт непересекающиеся
44
+ страницы на любом поле (id — стабильный tiebreaker); подтверждено на 440k записей с
45
+ совпадающим `updated`. Вторая (и последняя) правка ядра в релизе.
46
+
47
+ ### Changed — полигон-витрина: выразительный `salon-seed`
48
+
49
+ - **`bench/salon-seed.mjs` обогащён** ради наглядности API Reference/статьи: папки —
50
+ дерево 3 уровней (Каталог→Мужской/Женский зал→Борода и усы→Уход) вместо плоского
51
+ списка; навыки — каждый мастер умеет 2–5 РАЗНЫХ услуг (~1260 осмысленных вместо ~3000
52
+ вырожденных на одну услугу); цены комплексов с разбросом по оргу (топ прайса различим);
53
+ клиентам розданы теги `vip`/`telegram` (`has/hasAny/hasAll` = 400/800/100); части услуг
54
+ дан явный `description: null` (`isNull()` ≠ `exists(false)`).
55
+ - **§11 API Reference и SALON.md перегнаны построчно** под обогащённый полигон: имена
56
+ сущностей, числа-результаты и тайминги сняты живым прогоном воспроизводителей; keyset-
57
+ пример показывает пересечение страниц 0.
58
+
59
+ ## [0.17.0] — 2026-07-12
60
+
61
+ ### Changed — BREAKING: демо-домен booking переделан (позитивная доступность)
62
+
63
+ Библиотека не изменилась — переделан демо-домен booking (19 классов вместо 16;
64
+ источник правды — сид `lib/sql/seed.booking.sql`) и всё, что на нём стоит:
65
+ тесты, бенчи, полигоны, доки. Промежуточные дизайн-файлы `schema.booking.v2.*`
66
+ удалены из корня; `gen-seed.mjs` упразднён (сид редактируется напрямую).
67
+
68
+ - **Новые HUB**: `Person·Контрагент` (абстракт: `name`+`phone`, наследуют
69
+ Мастер/Клиент), `Location·Локация`, `Element·Элемент` (абстрактная каталожная
70
+ единица: `name`/`price` + правило `id = v5(Org, name)` объявлены ОДИН раз,
71
+ наследуются Услугой/Товаром/Комплексом), `Product·Товар`.
72
+ - **Смена категории**: Окно и Запись из HUB стали LINK:
73
+ - `slot·окно [Staff, Location, Schedule] {start_datetime, end_datetime}` —
74
+ интервал доступности мастера (смена), `id = v5(Staff, start_datetime)`;
75
+ - `booking·запись` — **наследник окна** (`ancestor: slot`): интервал и
76
+ v5-правило id приходят по наследству, свои концы — предмет-союз
77
+ `Service|Product|Complex` + `Customer?`. Двойная бронь мертва самим id
78
+ записи; окно и запись на одно время сосуществуют (classId в формуле).
79
+ - **Инверсия модели доступности**: негативная (Slot-сетка + busy off/hold/booking
80
+ + shift-ростер) → **позитивная** (окно есть = мастер доступен; свободно =
81
+ внутри окна и нет пересекающейся записи). Классы `busy`, `shift`, `item`,
82
+ HUB `Slot`/`Booking` — удалены.
83
+ - **Запись самодостаточна**: сама несёт интервал (длинная услуга = один интервал,
84
+ мульти-окна не нужны), предмет и клиента. «4 руки» = две записи. Перенос =
85
+ пересоздание (id жёстко держит пару мастер+старт). Отмена = tombstone-delete;
86
+ повторная бронь пары воскрешает id с историей. Статусы
87
+ (created/…/no_show), source, total, qty и снимок цены — удалены: история —
88
+ версии, цена — из предмета (`asOf` для задним числом).
89
+ - **Каталог**: принадлежность папке — LINK `content·содержимое [Folder, союз]`
90
+ (M:N + order) вместо Folder-конца; состав комплекса `compo·состав` принимает
91
+ союз `Service|Product`; вложенность папок осталась HUB-концом `Folder⤴`.
92
+ `skill·навык` сужен до `[Staff, Service]` (+`level`) — умение комплекса
93
+ вычислимо по составу. Новый `address·адрес [Staff, Location]`.
94
+ - **Упрощения атрибутов**: `price` — число (record по валютам удалён),
95
+ `Schedule {name, year, month}` (месячная модель), у Мастера/Клиента общие
96
+ поля уехали в Контрагента; `Complex` переопределяет унаследованный `price`
97
+ в `optional` (демонстрация «замена правила целиком»).
98
+ - Тесты переписаны под модель (**167 тестов**, было 165): `idgen.test.ts` —
99
+ наследование v5-правила через два уровня (booking→slot→link, DeluxeBooking),
100
+ v5 из пары концов (адрес) и конца+поля (запись); `real-life.test.ts` — сцены
101
+ переноса-пересоздания, воскрешения id, no-show пометкой; `salon.test.ts` —
102
+ 21 акт на новой модели; has/hasAny/hasAll переехали на колонку tags
103
+ (массивов в data демо-схемы больше нет).
104
+ - Полигон `v1.salondemo` (~1M строк) пересеян генератором
105
+ новой модели; SALON.md, README §3, TESTPLAN.md — перегнаны с живых прогонов.
106
+
107
+ ## [0.16.0] — 2026-07-12
108
+
109
+ ### Changed — BREAKING: глаголы записи create/update, слот unset, id считает Schema
110
+
111
+ - **`set()` разбит на `create()` / `update()`** — мина «`Класс()` → INSERT,
112
+ `Класс({})` → UPDATE всех» мертва, намерение всегда явное:
113
+ - **`create(data?)`** — INSERT; известный id (шаг `Класс(id)`, `data.id` или
114
+ вычисленный v5) уже существует → новая версия (идемпотентный create,
115
+ REST-PUT семантика). Фильтр-объект перед create — ошибка ПОСТРОЕНИЯ
116
+ (`create() takes no filter`); create на pivot-шаге — ошибка.
117
+ - **`update(data?)`** — новая версия КАЖДОГО найденного путём;
118
+ `Класс()` ≡ `Класс({})` — «все в границах контекста»; не найдено → `[]`,
119
+ update НИКОГДА не создаёт.
120
+ - `set()` бросает подсказку `set() split into create()/update() (0.16.0)`.
121
+ - **Слот-снятие переименовано: `.Класс.delete()` → `.Класс.unset()`** — развели
122
+ «удалить сущность» (`delete()`) и «снять конец связи» (`unset()`); старое имя
123
+ бросает подсказку. Слот теперь требует глагол записи ПЕРЕД собой:
124
+ `…create(…).Класс.set(x)` / `…update(…).Класс.unset()`; слот без операции —
125
+ ошибка `link slot needs a write` (раньше слот молча создавал `set({})`).
126
+ - **Генерация id по Schema** — `attributes.id`:
127
+ - `"uuid"` / `{type:'uuid'}` → **v4** (random, как раньше — дефолт);
128
+ - `{type:'uuid', generate: 7}` → **v7** (unix-время в старших битах — вставки
129
+ ложатся в хвост btree-индекса);
130
+ - `{type:'uuid', generate: 5, from:[…]}` → **v5, детерминированный**:
131
+ `uuidv5("pgSchema:partition:класс:значения from")`; `from` — имена
132
+ ОБЯЗАТЕЛЬНЫХ концов `Schema.links` (класс или полное имя союза
133
+ `'Service|Complex'`; значение — id конца) и/или скалярных полей `data`.
134
+ Явный id у v5-класса запрещён (`computes id`). Свойства: **create
135
+ идемпотентен** (та же комбинация → та же сущность, дубль невозможен даже в
136
+ гонке — двойная бронь мертва на уровне схемы) и **id известен ДО создания**
137
+ (формула открыта; namespace letopis `c7a2f9d4-3b61-4e8a-9f05-8d2c1e6b7a90`).
138
+ - Батч: multi-VALUES-склейка для v5-классов отключена (id нужны концы) —
139
+ такие планы исполняются поштучно, семантика та же.
140
+ - **Наследование attributes по иерархии Schema**: при загрузке реестра attributes
141
+ класса собираются по цепочке `ancestor` — потомок ПОВЕРХ предка, переопределение
142
+ поля = замена правила целиком (не слияние). Правило `id` (генерация) наследуется
143
+ так же; `links` НЕ наследуются (концы объявляются каждым классом). Валидатор и
144
+ fieldTypes компилируются из слитых attributes.
145
+ - **Демо-схема booking целиком на generate** — с наследованием: `Entity` и `link`
146
+ объявляют дефолт `{generate: 7}`, v7-классы (Org/Staff/Folder/Customer/Schedule/
147
+ Booking/item) наследуют его без собственного правила; v5-классы переопределяют:
148
+ `busy ← v5(Slot, Staff)`; `skill ← v5(Staff, Service|Complex)`;
149
+ `shift ← v5(Schedule, Staff)`; `compo ← v5(Complex, Service)`;
150
+ `Slot ← v5(Schedule, data.start)`; `Service`/`Complex` ← `v5(Org, data.name)`
151
+ (имя уникально в организации). Полигон `v1.salondemo` обновлён
152
+ на месте (UPDATE Schema; данные нетронуты).
153
+ - Тесты мигрированы (все `set`→`create`/`update`, слот `.delete`→`.unset`,
154
+ фикс-id v5-классов → вычисляемые); новый **`test/idgen.test.ts`** — генерация
155
+ id на боевых классах Schema (v4/v7/v5 из поля и концов, гонка без локов,
156
+ наследование attributes/id-правила, схема-гарды). SALON.md и демо-полигон
157
+ перегнаны на 0.16.0. Итого **165 тестов**.
158
+
159
+ ## [0.15.0] — 2026-07-12
160
+
161
+ ### Changed — BREAKING: единый закон пути, слоты связей, pivot, entity()
162
+ - **Один закон навигации для чтения И записи**: цепочка всегда подчиняется переходам
163
+ `Schema.links`; недопустимый переход — ошибка СИНХРОННО при построении (не в терминале).
164
+ Мультиродительский «контекст-сбор» через невалидные HUB→HUB прыжки удалён.
165
+ - **Слоты связей** вместо `.link()`: конец связки задаётся `.Класс.set(target)` ПОСЛЕ
166
+ `.set(данные)` — свойство-класс БЕЗ вызова. Владелец создаваемой связки — из валидного
167
+ пути, прочие концы — слотами. `target` = id | Row | **вложенная цепочка** (исполняется в
168
+ той же транзакции, обязана дать ровно одну сущность класса конца — «создать И привязать»).
169
+ `.Класс.delete()` снимает optional-конец; союз-конец `[A|B]` замещается слотом целиком.
170
+ **`.link()` УДАЛЁН** (бросает подсказку).
171
+ - **pivot**: повтор LINK-класса в пути — возврат к тому же узлу (ветвление к другому концу
172
+ + дофильтровка AND). `db.Запись(з).позиция().Услуга(у).позиция().Сотрудник(вася)` —
173
+ «позиции записи з с услугой у у мастера вася» одной цепочкой.
174
+ - **`entity(x)`** (Chain и db): вставить узел-переменную/паттерн (`db.позиция()`) или Row в
175
+ путь; та же переменная повторно — явный pivot; `db.entity(row).…` — старт с готовой строки.
176
+ - Тесты мигрированы на слоты; **`test/salon.test.ts`** — тест-план «работа салона»
177
+ (21 акт), покрывающий ВСЕ 145 публичных точек API + матрица покрытия. Итого 153 теста.
178
+
179
+ ## [0.14.0] — 2026-07-12
180
+
181
+ ### Changed — Schema.links v2: концы связей объектами
182
+ - **Конец связи — объект**: `{"class":"Org","cardinality":1}` /
183
+ `{"classes":["Service","Complex"],"cardinality":1}` (союз ролей) /
184
+ `{"class":"Staff","optional":true,…}`. `'Entity'`-полиморф в новых схемах не
185
+ используется — классы концов называются явно. **Колонка `Schema.links` — `jsonb`**
186
+ (настоящий массив объектов; в новых схемах). Старые схемы с `text[]` читаются тем же
187
+ кодом по прежним правилам (legacy-строки `'Org'`, `'Entity'`-полиморф).
188
+ - **Валидация строгая на двух уровнях** (либа `write.ts` + БД-триггер
189
+ `entity_check`): жадный матчинг ключей `Entity.links` по порядку объявления
190
+ концов; обязательный конец без ключа → `requires end "Service|Complex"`;
191
+ связь вне концов → `has stray link(s)` (раньше лишние молчали). Голый SQL
192
+ мимо либы ловится триггером так же.
193
+ - **Демо-домен уточнён**: `позиция = [Запись, {Услуга|Комплекс}, Сотрудник?]` —
194
+ три конца, владелец строго Запись; состав комплекса — новый LINK-класс
195
+ **`compo`/«состав» = [Комплекс, Услуга]** (раньше ездил на позиции);
196
+ `навык = [Сотрудник, {Услуга|Комплекс}]`; `занятость = [Сотрудник, Окно, Запись?]`
197
+ (третий конец легализован — раньше жил контрабандой); `Запись = [Клиент?]`
198
+ (ручная бронь без клиента). `cardinality` — зарезервировано, пока не проверяется.
199
+ - Демо-полигон мигрирован НА МЕСТЕ (ALTER links → jsonb + apply поверх):
200
+ данные Entity нетронуты (1 112 952), схема идентична booking.
201
+ - `lib/scripts/gen-seed.mjs` — сид Schema теперь реально генерится из
202
+ `schema.booking.v2.json` (комментарий «файл сгенерирован» стал правдой);
203
+ `schema-sync` сравнивает концы канонически (объект ⇔ JSON-текст в text[]).
204
+
5
205
  ## [0.13.0] — 2026-07-11
6
206
 
7
207
  ### Added — `up()`: одна точка входа
@@ -22,8 +222,8 @@
22
222
 
23
223
  ### Changed
24
224
  - Dev-контейнер переименован: образ `clockz-db` → **`letopis-db`**, контейнер
25
- `clockz-timescale` → **`letopis-timescale`** (данные перенесены дампом, полигон
26
- `v1.article` 1 112 952 строк цел).
225
+ `clockz-timescale` → **`letopis-timescale`** (данные перенесены дампом, демо-полигон
226
+ 1 112 952 строк цел).
27
227
  - Шаблоны переехали `db/*.sql` → `lib/sql/`, докер-файлы `db/docker/` → `lib/docker/`;
28
228
  `db/apply.mjs` остался CLI-обёрткой и читает шаблоны из `lib/sql/`.
29
229
 
@@ -42,7 +242,7 @@
42
242
  достраивает и о версиях не знает. Канал `pg_notify`/LISTEN = полное имя схемы
43
243
  (`pg_notify('v1.booking', …)` — строковый литерал; LISTEN-идентификатор postgres.js
44
244
  кавычит сам).
45
- - Все тестовые/бенч-схемы переехали: `v1.booking`, `v1.article`, `v1.acl`, `v1.depth`,
245
+ - Все тестовые/бенч-схемы переехали: `v1.booking`, `v1.acl`, `v1.depth`,
46
246
  `v1.plan`, `v1.wave4`, `v1.bench`; безверсионные дропнуты.
47
247
 
48
248
  ## [0.11.0] — 2026-07-11