letopis 1.2.0 → 1.4.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 +12 -7
- package/AGENT-CHEATSHEET.md +12 -7
- package/CHANGELOG.md +213 -0
- package/README.en.md +70 -21
- package/README.md +59 -18
- package/dist/index.js +27 -7
- package/dist/project.d.ts +25 -3
- package/dist/project.js +122 -21
- package/dist/sync.js +16 -1
- package/dist/up.d.ts +17 -0
- package/dist/up.js +64 -0
- package/dist/watch.js +28 -3
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/sql/10-core.sql +3 -1
- package/sql/50-acl.sql +5 -1
- package/sql/60-write.sql +166 -25
- package/sql/88-derived.sql +83 -3
- package/sql/90-time.sql +113 -22
- package/sql/99-revision.sql +3 -2
- package/sql/README.md +3 -1
package/sql/90-time.sql
CHANGED
|
@@ -80,13 +80,22 @@ begin
|
|
|
80
80
|
return false;
|
|
81
81
|
end $$;
|
|
82
82
|
|
|
83
|
+
-- Действующая роль сеанса, под которой вызывающий работает вне функций владельца: роль SET ROLE (и параметра запуска
|
|
84
|
+
-- role), иначе роль входа. Внутри функции владельца current_user — владелец, а session_user — роль входа, даже если
|
|
85
|
+
-- поверх неё поставлена роль приложения: привилегированный вход с ролью приложения выглядел бы администратором
|
|
86
|
+
-- (автоматическое ревью коммита 1734d21). Подменить её нельзя: SET ROLE проверяет членство.
|
|
87
|
+
create or replace function @SCHEMA@.session_role() returns name
|
|
88
|
+
language sql stable
|
|
89
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
90
|
+
return coalesce(nullif(current_setting('role'), 'none'), session_user)::name;
|
|
91
|
+
|
|
83
92
|
-- Роль подключения — администраторская (суперпользователь или может стать владельцем): verify,
|
|
84
93
|
-- reset и maintain доступны ей и без сессии (CLI, обслуживание по расписанию).
|
|
85
94
|
create or replace function @SCHEMA@.admin_role() returns boolean
|
|
86
95
|
language sql stable
|
|
87
96
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
88
|
-
return exists (select 1 from pg_roles where rolname =
|
|
89
|
-
or pg_has_role(
|
|
97
|
+
return exists (select 1 from pg_roles where rolname = @SCHEMA@.session_role() and rolsuper)
|
|
98
|
+
or pg_has_role(@SCHEMA@.session_role(), '@OWNER_NAME@', 'SET');
|
|
90
99
|
|
|
91
100
|
-- Право letopis.* актора сессии по правилам System или администраторская роль подключения.
|
|
92
101
|
create or replace function @SCHEMA@.letopis_allowed(p_endpoint text) returns boolean
|
|
@@ -209,6 +218,9 @@ begin
|
|
|
209
218
|
end if;
|
|
210
219
|
perform @SCHEMA@.family_lock(cls, false);
|
|
211
220
|
cls := @SCHEMA@.class_row(l.class, l.tenant);
|
|
221
|
+
if cls.id is null then
|
|
222
|
+
perform @SCHEMA@.lt_fail('invalid_class', format('класс %s удалён', l.class));
|
|
223
|
+
end if;
|
|
212
224
|
res := cls.data->'resolved';
|
|
213
225
|
if coalesce((res->>'abstract')::boolean, false) then
|
|
214
226
|
perform @SCHEMA@.lt_fail('invalid_class', format('класс %s абстрактный', l.class));
|
|
@@ -256,6 +268,39 @@ end $$;
|
|
|
256
268
|
-- все версии удаляются, а на место версии 1 встаёт запись purge с числом стёртых версий и хэшем
|
|
257
269
|
-- последней. Право DELETE на класс по последнему снимку. История системных классов — аудит прав и
|
|
258
270
|
-- схемы: её не стирают.
|
|
271
|
+
-- Проектор источника ещё не прочёл одну из версий p_id арендатора p_tenant с номером до p_rev включительно (null — все
|
|
272
|
+
-- версии): курсор не прошёл её позицию (tx, seq), и снимок перестройки её не учёл (ревью безопасности №3, №5 — purge;
|
|
273
|
+
-- хвост ревью №3 — обрезка истории). Проверяется каждая версия (хвост ревью доработок после 1.2.0): у более поздней
|
|
274
|
+
-- версии бывает меньший xid, а снимок перестройки учитывает транзакции, зафиксированные до него, — непрочитанной может
|
|
275
|
+
-- оказаться и версия между самой поздней по (tx, seq) и последней. Проектор касается арендатора, у которого есть его
|
|
276
|
+
-- производный класс (derivedBy — имя проектора): брошенный проектор с удалёнными производными классами не держит, — и
|
|
277
|
+
-- строки, у которой хоть одна из этих версий — в классах его источника: уход из семейства источников сменой класса он
|
|
278
|
+
-- тоже читает. Курсор «tx:seq~снимок»; чекпоинт без from (до 1.2.0) касается всех классов. Записи purge, trim и reset —
|
|
279
|
+
-- не версии.
|
|
280
|
+
create or replace function @SCHEMA@.projector_unread(p_tenant uuid, p_id uuid, p_rev int) returns boolean
|
|
281
|
+
language sql stable
|
|
282
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
283
|
+
return exists (select 1 from @SCHEMA@.secret s
|
|
284
|
+
cross join lateral (select split_part(s.meta->>'cursor', '~', 1) as head,
|
|
285
|
+
nullif(split_part(s.meta->>'cursor', '~', 2), '') as snap) k
|
|
286
|
+
where s.kind = 'PROJECTOR' and s.account = '@SYSTEM@'::uuid and s.deleted_at is null
|
|
287
|
+
and coalesce(s.meta->>'cursor', '') <> ''
|
|
288
|
+
and exists (select 1 from @SCHEMA@.entity x where x.class = 'Class' and x.tenant = p_tenant
|
|
289
|
+
and x.data->'resolved'->>'derivedBy' = s.ident)
|
|
290
|
+
and (not s.meta ? 'from'
|
|
291
|
+
or exists (select 1 from (select distinct v.class from @SCHEMA@.log v
|
|
292
|
+
where v.id = p_id and (p_rev is null or v.rev <= p_rev)
|
|
293
|
+
and v.op not in ('purge', 'trim', 'reset')) c
|
|
294
|
+
where coalesce((@SCHEMA@.class_row(c.class, p_tenant)).data->'resolved'->'ancestors', jsonb_build_array(c.class))
|
|
295
|
+
?| array(select jsonb_array_elements_text(s.meta->'from'))))
|
|
296
|
+
and exists (select 1 from @SCHEMA@.log v
|
|
297
|
+
where v.id = p_id and (p_rev is null or v.rev <= p_rev) and v.op not in ('purge', 'trim', 'reset')
|
|
298
|
+
and (v.tx, v.seq) > (split_part(k.head, ':', 1)::xid8, split_part(k.head, ':', 2)::bigint)
|
|
299
|
+
and not coalesce(pg_visible_in_snapshot(v.tx, k.snap::pg_snapshot), false)));
|
|
300
|
+
|
|
301
|
+
-- до 1.4 проверялась одна позиция: надгробие у purge, самая поздняя по (tx, seq) версия у стыка обрезки
|
|
302
|
+
drop function if exists @SCHEMA@.projector_behind(uuid, text[], xid8, bigint);
|
|
303
|
+
|
|
259
304
|
create or replace function @SCHEMA@.purge(p_id uuid, p_confirm boolean default false) returns jsonb
|
|
260
305
|
language plpgsql volatile security definer
|
|
261
306
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -279,22 +324,9 @@ begin
|
|
|
279
324
|
if not @SCHEMA@.acl_row_ok(@SCHEMA@.acl_plan('DELETE'), l.class, l.id, l.owner, l.tenant, l.tags, l.data, l.links) then
|
|
280
325
|
perform @SCHEMA@.lt_fail('acl_denied', format('нет права DELETE на класс %s', l.class));
|
|
281
326
|
end if;
|
|
282
|
-
-- проектор источника,
|
|
283
|
-
-- узнал бы — вклад строки остался бы в итогах навсегда (ревью безопасности №3, №5)
|
|
284
|
-
|
|
285
|
-
-- классами purge не держит. Курсор «tx:seq~снимок»: транзакции, видимые в снимке, учла перестройка. Чекпоинт без from
|
|
286
|
-
-- (до 1.2.0) касается всех классов.
|
|
287
|
-
if exists (select 1 from @SCHEMA@.secret s
|
|
288
|
-
cross join lateral (select split_part(s.meta->>'cursor', '~', 1) as head,
|
|
289
|
-
nullif(split_part(s.meta->>'cursor', '~', 2), '') as snap) k
|
|
290
|
-
where s.kind = 'PROJECTOR' and s.account = '@SYSTEM@'::uuid and s.deleted_at is null
|
|
291
|
-
and coalesce(s.meta->>'cursor', '') <> ''
|
|
292
|
-
and exists (select 1 from @SCHEMA@.entity x where x.class = 'Class' and x.tenant = l.tenant
|
|
293
|
-
and x.data->'resolved'->>'derivedBy' = s.ident)
|
|
294
|
-
and (not s.meta ? 'from' or coalesce((@SCHEMA@.class_row(l.class, l.tenant)).data->'resolved'->'ancestors',
|
|
295
|
-
jsonb_build_array(l.class)) ?| array(select jsonb_array_elements_text(s.meta->'from')))
|
|
296
|
-
and (l.tx, l.seq) > (split_part(k.head, ':', 1)::xid8, split_part(k.head, ':', 2)::bigint)
|
|
297
|
-
and not coalesce(pg_visible_in_snapshot(l.tx, k.snap::pg_snapshot), false)) then
|
|
327
|
+
-- проектор источника, который ещё не прочёл какую-то версию строки (в том числе надгробие этой транзакции), об
|
|
328
|
+
-- удалении не узнал бы — вклад строки остался бы в итогах навсегда (ревью безопасности №3, №5)
|
|
329
|
+
if @SCHEMA@.projector_unread(l.tenant, p_id, null) then
|
|
298
330
|
perform @SCHEMA@.lt_fail('projector_pending', 'проектор источника ещё не прочёл удаление: purge — после его run()');
|
|
299
331
|
end if;
|
|
300
332
|
select count(*)::int into n from @SCHEMA@.log x where x.id = p_id;
|
|
@@ -330,6 +362,7 @@ declare
|
|
|
330
362
|
dplan jsonb;
|
|
331
363
|
r @SCHEMA@.log;
|
|
332
364
|
first @SCHEMA@.log;
|
|
365
|
+
nxt record;
|
|
333
366
|
j int;
|
|
334
367
|
h bytea;
|
|
335
368
|
k int;
|
|
@@ -374,6 +407,13 @@ begin
|
|
|
374
407
|
select x.* into first from @SCHEMA@.log x where x.id = r.id order by x.rev limit 1;
|
|
375
408
|
-- до момента — только сама запись trim: стирать нечего
|
|
376
409
|
continue when j = 1 and first.op = 'trim';
|
|
410
|
+
-- проектор источника, который ещё не прочёл стираемые версии или первую оставшуюся: читая её, он возьмёт прошлую
|
|
411
|
+
-- версию для before, а её уже не будет — свёртка прибавила бы новую, не вычитая прежнюю (хвост ревью безопасности
|
|
412
|
+
-- №3); каждая версия до первой оставшейся — у более поздней версии бывает меньший xid
|
|
413
|
+
select x.rev into nxt from @SCHEMA@.log x where x.id = r.id and x.rev > j order by x.rev limit 1;
|
|
414
|
+
if @SCHEMA@.projector_unread(r.tenant, r.id, nxt.rev) then
|
|
415
|
+
perform @SCHEMA@.lt_fail('projector_pending', 'проектор источника ещё не прочёл версии: trimHistory — после его run()');
|
|
416
|
+
end if;
|
|
377
417
|
select x.hash into h from @SCHEMA@.log x where x.id = r.id and x.rev = j;
|
|
378
418
|
with d as (delete from @SCHEMA@.log x where x.id = r.id and x.rev <= j returning 1)
|
|
379
419
|
select count(*)::int into k from d;
|
|
@@ -434,6 +474,9 @@ begin
|
|
|
434
474
|
cls := @SCHEMA@.class_row(o.class, o.tenant);
|
|
435
475
|
perform @SCHEMA@.family_lock(cls, false);
|
|
436
476
|
cls := @SCHEMA@.class_row(o.class, o.tenant);
|
|
477
|
+
if cls.id is null then
|
|
478
|
+
perform @SCHEMA@.lt_fail('invalid_class', format('класс %s удалён', o.class));
|
|
479
|
+
end if;
|
|
437
480
|
res := cls.data->'resolved';
|
|
438
481
|
if res ? 'derivedBy' then
|
|
439
482
|
perform @SCHEMA@.lt_fail('derived_class', format('класс %s производный: его строки пишет проектор %s', o.class, res->>'derivedBy'));
|
|
@@ -1034,6 +1077,54 @@ begin
|
|
|
1034
1077
|
order by l.tx, l.seq limit p_limit;
|
|
1035
1078
|
end $$;
|
|
1036
1079
|
|
|
1080
|
+
-- Уход строки из классов подписки сменой класса без смены id (хвост ревью безопасности №3): версия пишется уже с новым
|
|
1081
|
+
-- классом, и подписка по классам её не видит. Здесь — те же окно, горизонт и снимок, что у watch_read (и верхняя граница
|
|
1082
|
+
-- окна p_upto — последняя строка полной пачки watch_read), а строки — синтетические надгробия прежней версии: класс,
|
|
1083
|
+
-- концы и данные прежние; номер, время, автор, tx и seq — версии смены класса; вид delete; агента и причины нет. Только
|
|
1084
|
+
-- если нового класса нет в p_classes, а прежнюю версию актор видит (row_visible с его планом READ): о строке, которой он
|
|
1085
|
+
-- не читал, подписчик не узнает. Функция владельца: версию с новым классом актору видеть не нужно. Подключение в обход
|
|
1086
|
+
-- RLS (суперпользователь, BYPASSRLS, член роли-владельца с наследованием — connect() пускает его только с
|
|
1087
|
+
-- allowBypassRls) видит в watch_read журнал всех арендаторов, поэтому и события ухода получает все, с автором (хвост
|
|
1088
|
+
-- ревью доработок после 1.2.0). Роль — действующая роль сеанса (session_role): та, к которой watch_read применяет RLS.
|
|
1089
|
+
create or replace function @SCHEMA@.watch_leaving(p_tx xid8, p_seq bigint, p_classes text[], p_limit int,
|
|
1090
|
+
p_snap pg_snapshot default null, p_upto_tx xid8 default null, p_upto_seq bigint default null)
|
|
1091
|
+
returns setof @SCHEMA@.log
|
|
1092
|
+
language plpgsql stable security definer
|
|
1093
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
1094
|
+
as $$
|
|
1095
|
+
declare
|
|
1096
|
+
horizon xid8 := pg_snapshot_xmin(pg_current_snapshot());
|
|
1097
|
+
a jsonb;
|
|
1098
|
+
bypass boolean;
|
|
1099
|
+
begin
|
|
1100
|
+
if p_classes is null then
|
|
1101
|
+
return;
|
|
1102
|
+
end if;
|
|
1103
|
+
bypass := exists (select 1 from pg_roles r where r.rolname = @SCHEMA@.session_role() and (r.rolsuper or r.rolbypassrls))
|
|
1104
|
+
or pg_has_role(@SCHEMA@.session_role(), '@OWNER_NAME@', 'USAGE');
|
|
1105
|
+
a := case when not bypass then @SCHEMA@.acl_actor('READ') end;
|
|
1106
|
+
if a is null and not bypass then
|
|
1107
|
+
return;
|
|
1108
|
+
end if;
|
|
1109
|
+
-- новую версию актор не видит — надгробие при любом p_classes и без автора: перебором классов подписки он узнал бы её
|
|
1110
|
+
-- класс (автоматическое ревью доработок после 1.2.0)
|
|
1111
|
+
return query select l.seq, l.tx, p.id, l.rev, p.class, p.cv, p.tenant, p.owner, p.links, p.data, p.tags, l.at,
|
|
1112
|
+
case when v.seen then l.author end, null::text, 'delete'::text, null::text, null::uuid, null::bytea, null::bytea, null::jsonb
|
|
1113
|
+
from @SCHEMA@.log l
|
|
1114
|
+
cross join lateral (select x.* from @SCHEMA@.log x where x.id = l.id and x.rev < l.rev and x.op not in ('purge', 'trim', 'reset')
|
|
1115
|
+
order by x.rev desc limit 1) p
|
|
1116
|
+
cross join lateral (select bypass or @SCHEMA@.row_visible(l.tenant, l.class, l.id, l.owner, l.tags, l.data, l.links,
|
|
1117
|
+
(a->>'tenant')::uuid, (a->>'account')::uuid, a->'plan') as seen) v
|
|
1118
|
+
where (l.tx, l.seq) > (p_tx, p_seq) and l.tx < horizon
|
|
1119
|
+
and (p_upto_tx is null or (l.tx, l.seq) <= (p_upto_tx, p_upto_seq))
|
|
1120
|
+
and l.op = 'reclass' and l.moved is null and (not (l.class = any (p_classes)) or not v.seen)
|
|
1121
|
+
and p.class = any (p_classes) and p.op <> 'delete'
|
|
1122
|
+
and (p_snap is null or not pg_visible_in_snapshot(l.tx, p_snap))
|
|
1123
|
+
and (bypass or @SCHEMA@.row_visible(p.tenant, p.class, p.id, p.owner, p.tags, p.data, p.links,
|
|
1124
|
+
(a->>'tenant')::uuid, (a->>'account')::uuid, a->'plan'))
|
|
1125
|
+
order by l.tx, l.seq limit p_limit;
|
|
1126
|
+
end $$;
|
|
1127
|
+
|
|
1037
1128
|
-- Точка подписки «с этого момента»: курсор — горизонт завершённых транзакций, снимок — чтобы не
|
|
1038
1129
|
-- отдавать транзакции, зафиксированные до подписки, но ещё не ушедшие за горизонт.
|
|
1039
1130
|
drop function if exists @SCHEMA@.watch_head();
|
|
@@ -1052,11 +1143,11 @@ declare f text;
|
|
|
1052
1143
|
begin
|
|
1053
1144
|
foreach f in array array[
|
|
1054
1145
|
'history_default()', 'history_policy(jsonb)', 'history_iv(text)', 'history_horizon(jsonb)',
|
|
1055
|
-
'history_keep(jsonb, timestamp, timestamp, timestamp)', 'admin_role()', 'letopis_allowed(text)', 'session_expires()', 'signal_mode()', 'signal_key()', 'class_stamp()',
|
|
1056
|
-
'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
|
|
1146
|
+
'history_keep(jsonb, timestamp, timestamp, timestamp)', 'session_role()', 'admin_role()', 'letopis_allowed(text)', 'session_expires()', 'signal_mode()', 'signal_key()', 'class_stamp()',
|
|
1147
|
+
'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'projector_unread(uuid, uuid, int)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
|
|
1057
1148
|
'rekey(uuid, jsonb, jsonb)', 'rekey_class(text, jsonb)', 'account_purge(uuid)', 'reset_mark(uuid, text, uuid)',
|
|
1058
1149
|
'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int)', 'verify(boolean)',
|
|
1059
|
-
'watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean)', 'watch_head()'] loop
|
|
1150
|
+
'watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean)', 'watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint)', 'watch_head()'] loop
|
|
1060
1151
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
1061
1152
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
1062
1153
|
end loop;
|
|
@@ -1065,6 +1156,6 @@ end $$;
|
|
|
1065
1156
|
grant execute on function @SCHEMA@.restore(uuid), @SCHEMA@.purge(uuid, boolean), @SCHEMA@.trim_history(uuid[], text[], timestamptz),
|
|
1066
1157
|
@SCHEMA@.rekey(uuid, jsonb, jsonb), @SCHEMA@.rekey_class(text, jsonb), @SCHEMA@.account_purge(uuid),
|
|
1067
1158
|
@SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int), @SCHEMA@.verify(boolean),
|
|
1068
|
-
@SCHEMA@.session_expires(), @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean), @SCHEMA@.watch_head(),
|
|
1159
|
+
@SCHEMA@.session_expires(), @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean), @SCHEMA@.watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint), @SCHEMA@.watch_head(),
|
|
1069
1160
|
@SCHEMA@.history_default(), @SCHEMA@.history_policy(jsonb), @SCHEMA@.history_iv(text),
|
|
1070
1161
|
@SCHEMA@.signal_mode(), @SCHEMA@.signal_key(), @SCHEMA@.class_stamp() to @APP@;
|
package/sql/99-revision.sql
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
-- letopis 1.0 — штамп ревизии движка (план, §2.2, этап 1, п. 11): последний оператор наката.
|
|
2
|
-
-- Номер совпадает с DDL_REVISION в lib/src/types.ts (проверяет check-docs).
|
|
3
|
-
|
|
2
|
+
-- Номер совпадает с DDL_REVISION в lib/src/types.ts (проверяет check-docs). С 1.4 в штампе ещё версия пакета и хэш
|
|
3
|
+
-- файлов ядра (engine, sql): connect() предупреждает, если функции схемы ставила другая версия движка той же ревизии.
|
|
4
|
+
comment on schema @SCHEMA@ is 'letopis ddl_revision=6 engine=@ENGINE_VERSION@ sql=@ENGINE_SQL@';
|
package/sql/README.md
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
| `@HISTORY_DEFAULT@` | политика хранения по умолчанию `up({ history })` литералом JSON или `'null'` — константа установки |
|
|
18
18
|
| `@RESERVED_NAMES@` | методы цепочки — `RESERVED_CLASS_NAMES` из `lib/src/types.ts`, литералами через запятую |
|
|
19
19
|
| `@SIGNALS@` | режим сигналов `class`, `tenant`, `opaque` или `none` (план 1.1, §2.2): опция `up({ signals })`, иначе сохранённый в `meta.signals` строки ключа `secret`, иначе `class` — константа установки |
|
|
20
|
+
| `@ENGINE_VERSION@` | версия пакета (`lib/package.json`) — штамп движка в комментарии схемы (`99-revision.sql`) |
|
|
21
|
+
| `@ENGINE_SQL@` | 16 знаков sha256 исходного текста файлов `NN-*.sql` до подстановок (переводы строк — LF): `connect()` сверяет его с файлами своего пакета |
|
|
20
22
|
|
|
21
23
|
## Колонки снимка версии
|
|
22
24
|
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
| LT020 | `confirm_mismatch` | подтверждение сброса не совпадает с именем схемы (этап 6) |
|
|
54
56
|
| LT021 | `derived_class` | строки производного класса (`derivedBy`) пишет и удаляет только его проектор; на них не ссылаются концы (план 1.1, §2.4) |
|
|
55
57
|
| LT022 | `aggregate_field` | агрегатное поле (`aggregate`) считает база: приложение и сырой SQL его не передают и не меняют (план 1.1, §2.4) |
|
|
56
|
-
| LT023 | `projector_pending` | `purge` удалённого объекта, чьё надгробие ещё не прочёл проектор его класса (ревью безопасности №3, №5) |
|
|
58
|
+
| LT023 | `projector_pending` | `purge` удалённого объекта, чьё надгробие ещё не прочёл проектор его класса (ревью безопасности №3, №5), или `trim_history`, чью первую оставшуюся версию он ещё не прочёл (хвост ревью №3) |
|
|
57
59
|
|
|
58
60
|
## Порядок файлов
|
|
59
61
|
|