letopis 0.20.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/AGENT-CHEATSHEET.en.md +368 -0
  2. package/AGENT-CHEATSHEET.md +354 -0
  3. package/CHANGELOG.md +348 -0
  4. package/MIGRATION.md +190 -0
  5. package/README.en.md +1937 -0
  6. package/README.md +1493 -3466
  7. package/dist/acl.d.ts +26 -50
  8. package/dist/acl.js +22 -267
  9. package/dist/admin.d.ts +138 -0
  10. package/dist/admin.js +170 -0
  11. package/dist/auth.d.ts +120 -73
  12. package/dist/auth.js +121 -306
  13. package/dist/cache.d.ts +73 -0
  14. package/dist/cache.js +148 -0
  15. package/dist/chain.d.ts +124 -191
  16. package/dist/chain.js +369 -551
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +164 -0
  19. package/dist/demo/booking.d.ts +289 -0
  20. package/dist/demo/booking.js +159 -0
  21. package/dist/errors.d.ts +29 -0
  22. package/dist/errors.js +70 -0
  23. package/dist/import.d.ts +179 -0
  24. package/dist/import.js +792 -0
  25. package/dist/index.d.ts +172 -26
  26. package/dist/index.js +304 -178
  27. package/dist/jsonschema.d.ts +22 -0
  28. package/dist/jsonschema.js +167 -0
  29. package/dist/load.d.ts +76 -0
  30. package/dist/load.js +884 -0
  31. package/dist/model.d.ts +166 -0
  32. package/dist/model.js +224 -0
  33. package/dist/ops.d.ts +7 -6
  34. package/dist/ops.js +7 -51
  35. package/dist/pglite.d.ts +22 -0
  36. package/dist/pglite.js +45 -0
  37. package/dist/registry.d.ts +57 -0
  38. package/dist/registry.js +82 -0
  39. package/dist/sql.d.ts +59 -142
  40. package/dist/sql.js +568 -654
  41. package/dist/sync.d.ts +31 -0
  42. package/dist/sync.js +108 -0
  43. package/dist/tx.d.ts +129 -8
  44. package/dist/tx.js +300 -73
  45. package/dist/typed.d.ts +97 -0
  46. package/dist/typed.js +1 -0
  47. package/dist/types.d.ts +71 -250
  48. package/dist/types.js +27 -108
  49. package/dist/up.d.ts +140 -47
  50. package/dist/up.js +339 -267
  51. package/dist/uuid.d.ts +21 -6
  52. package/dist/uuid.js +48 -64
  53. package/dist/validate.d.ts +24 -0
  54. package/dist/validate.js +251 -0
  55. package/dist/watch.d.ts +62 -0
  56. package/dist/watch.js +168 -0
  57. package/dist/write.d.ts +117 -74
  58. package/dist/write.js +658 -720
  59. package/llms.txt +26 -0
  60. package/package.json +49 -19
  61. package/sql/10-core.sql +136 -0
  62. package/sql/15-errors.sql +60 -0
  63. package/sql/20-context.sql +153 -0
  64. package/sql/30-validate.sql +423 -0
  65. package/sql/40-class.sql +259 -0
  66. package/sql/50-acl.sql +539 -0
  67. package/sql/60-write.sql +1369 -0
  68. package/sql/70-read.sql +245 -0
  69. package/sql/80-auth.sql +827 -0
  70. package/sql/90-time.sql +957 -0
  71. package/sql/95-seed.system.sql +178 -0
  72. package/sql/99-revision.sql +3 -0
  73. package/sql/README.md +56 -0
  74. package/sql/seed.booking.sql +39 -112
  75. package/dist/schema.d.ts +0 -15
  76. package/dist/schema.js +0 -351
  77. package/dist/sessions.d.ts +0 -32
  78. package/dist/sessions.js +0 -114
  79. package/dist/tables.d.ts +0 -105
  80. package/dist/tables.js +0 -248
  81. package/docker/Dockerfile +0 -40
  82. package/docker/start.sh +0 -18
  83. package/scripts/check-docs.mjs +0 -375
  84. package/scripts/gen-api-contract.mjs +0 -226
  85. package/scripts/gen-types.mjs +0 -350
  86. package/scripts/release-notes.mjs +0 -76
  87. package/scripts/schema-sync.mjs +0 -185
  88. package/sql/ddl.sql +0 -600
  89. package/sql/seed.auth.sql +0 -73
package/dist/acl.d.ts CHANGED
@@ -1,53 +1,29 @@
1
- /**
2
- * ACL по таблицам Resource/Rule. Pattern каждой категории — шаблон «своей» сущности:
3
- *
4
- * category='ACCOUNT' — шаблон Account: {"categories": "{A,B}"} — есть любая;
5
- * "!{A,B}" — нет ни одной; NULL — все аккаунты. Субъекты правил.
6
- * category='API' — шаблон адреса эндпоинта: {"endpoint": "v2.auth.apikey.*"};
7
- * маска: '.'-сегменты, '{a,b}' — альтернативы (вложенные ок), '*' — хвост 1+ сегментов.
8
- * category='READ' | 'WRITE' | 'DELETE' — ШАБЛОН СТРОКИ Entity (операция = категория):
9
- * ключи — реальные колонки (class, owner, account, tags, data, links, id, partition),
10
- * значения — литералы или "$account" (id текущего аккаунта, подставляется в запрос).
11
- * "class" — маска с lineage (класс или любой его предок); нет ключа/NULL — все строки.
12
- * Остальные ключи победившего allow-правила вливаются в SQL ДО сортировки и лимита —
13
- * пагинация/count/keyset работают по уже суженному множеству.
14
- *
15
- * Rule(субъект-alias → объект-alias, permission allow|deny, weight): правила никогда не
16
- * указывают на конкретный аккаунт — принадлежность группе вычисляется из categories.
17
- * Побеждает РОВНО ОДНО правило: max weight; при равном весе deny бьёт allow (равные
18
- * allow — детерминированно по alias ресурса); НЕТ правил — deny (deny-by-default).
19
- */
20
- import type { AclOp, AclDecision, ClassDef, Resource, Rule } from './types.js';
21
- import type { Registry } from './schema.js';
22
- import type { Tables } from './tables.js';
23
- export declare const ACL_OPS: readonly AclOp[];
24
- /** pattern.categories: "{A,B}" | "!{A,B}" | отсутствие/NULL (матч всех). */
25
- export declare function matchCategories(pattern: Record<string, unknown> | null, cats: string[]): boolean;
26
- export declare function matchMask(mask: string, value: string): boolean;
27
- export interface AclApi {
28
- /** Может ли аккаунт дёрнуть эндпоинт (Resource category='API'). */
29
- check(account: string | {
30
- id: string;
31
- }, endpoint: string): Promise<AclDecision>;
32
- /** Решение операции над классом: allow + остаточный шаблон строк (filter) | deny. */
33
- checkData(account: string | {
1
+ import type { Executor } from './tx.js';
2
+ export type AclOp = 'READ' | 'WRITE' | 'DELETE';
3
+ /** Решение по эндпоинту API. */
4
+ export interface AclDecision {
5
+ allow: boolean;
6
+ /** Победившее правило: id, группа и объект (alias ресурсов), вес, разрешение. */
7
+ rule?: {
34
8
  id: string;
35
- }, className: string, op: AclOp): Promise<AclDecision>;
36
- /**
37
- * Сбросить кэш Resource/Rule ЭТОГО фасада (db.acl.check/checkData).
38
- * NB: энфорсер цепочек под `enforceAcl` — отдельная подсистема; его пересобирает
39
- * `db.reloadSchema()` (реестр + ACL-резолвер), а не этот reload().
40
- */
41
- reload(): void;
9
+ group: string;
10
+ object: string;
11
+ weight: number;
12
+ permission: 'allow' | 'deny';
13
+ };
14
+ /** Код и сообщение запрещающего правила (поля code, message строки rule). */
15
+ code?: number;
16
+ message?: string;
42
17
  }
43
- interface AclSource {
44
- resources: Resource[];
45
- rules: Rule[];
18
+ /** Решение по операции над классом: разрешено и, если есть, условие на строки (шаблон колонок). */
19
+ export interface AclDataDecision {
20
+ allow: boolean;
21
+ filter?: Record<string, unknown>;
22
+ }
23
+ export interface AclApi {
24
+ /** Может ли актор вызвать эндпоинт (ресурсы категории API; правила System и арендатора). */
25
+ check(endpoint: string): Promise<AclDecision>;
26
+ /** Решение операции над классом для актора: allow и условие на строки (filter). */
27
+ checkData(className: string, op: AclOp): Promise<AclDataDecision>;
46
28
  }
47
- export declare function makeAcl(tables: Tables, registry: Registry): AclApi;
48
- /** Синхронный резолвер для цепочек: категории субъекта и правила — снимок на момент
49
- * компиляции (connect либо db.reloadSchema(), который пересобирает энфорсер). */
50
- export declare function compileEnforcer(src: AclSource, account: string, cats: string[], registry: Registry): (cls: ClassDef, op: AclOp) => AclDecision;
51
- /** Читаемая ошибка отказа для цепочек. */
52
- export declare function aclDenied(op: AclOp, clsId: string, d: AclDecision): Error;
53
- export {};
29
+ export declare function makeAcl(S: string, exec: Executor): AclApi;
package/dist/acl.js CHANGED
@@ -1,271 +1,26 @@
1
- export const ACL_OPS = ['READ', 'WRITE', 'DELETE'];
2
- // --- матчеры -----------------------------------------------------------------
3
- // START_CONTRACT: matchCategories
4
- // PURPOSE: Проверить шаблон pattern.categories ("{A,B}" | "!{A,B}" | NULL) против категорий аккаунта.
5
- // INPUTS: { pattern: object|null - шаблон ресурса ACCOUNT; cats: string[] - категории аккаунта }
6
- // OUTPUTS: { boolean - подходит ли субъект }
7
- // SIDE_EFFECTS: none
8
- // LINKS: M-ACL, V-M-ACL
9
- // END_CONTRACT: matchCategories
10
- /** pattern.categories: "{A,B}" | "!{A,B}" | отсутствие/NULL (матч всех). */
11
- export function matchCategories(pattern, cats) {
12
- const spec = pattern?.categories;
13
- if (typeof spec !== 'string')
14
- return true;
15
- const neg = spec.startsWith('!');
16
- const list = spec
17
- .replace(/^!/, '')
18
- .replace(/^\{/, '')
19
- .replace(/\}$/, '')
20
- .split(',')
21
- .map((s) => s.trim())
22
- .filter(Boolean);
23
- const hit = list.some((c) => cats.includes(c));
24
- return neg ? !hit : hit;
25
- }
26
- // START_CONTRACT: maskBody
27
- // PURPOSE: Скомпилировать тело маски в regex-фрагмент: '{a,b}' → альтернативы (рекурсивно), '*' → хвост из 1+ сегментов.
28
- // INPUTS: { mask: string - маска с '.'-сегментами }
29
- // OUTPUTS: { string - regex-исходник без якорей }
30
- // SIDE_EFFECTS: none
31
- // LINKS: M-ACL, V-M-ACL
32
- // END_CONTRACT: maskBody
33
- /** Маска → regex: '{a,b}' — альтернативы (рекурсивно), '*' — хвост из 1+ сегментов. */
34
- function maskBody(mask) {
35
- // START_BLOCK_MASK_COMPILE
36
- let out = '';
37
- for (let i = 0; i < mask.length; i++) {
38
- const ch = mask[i];
39
- if (ch === '{') {
40
- let depth = 1;
41
- let j = i + 1;
42
- for (; j < mask.length && depth; j++) {
43
- if (mask[j] === '{')
44
- depth++;
45
- else if (mask[j] === '}')
46
- depth--;
47
- }
48
- const inner = mask.slice(i + 1, j - 1);
49
- const parts = [];
50
- let d = 0;
51
- let start = 0;
52
- for (let k = 0; k <= inner.length; k++) {
53
- const c = inner[k];
54
- if (k === inner.length || (c === ',' && d === 0)) {
55
- parts.push(inner.slice(start, k));
56
- start = k + 1;
57
- }
58
- else if (c === '{')
59
- d++;
60
- else if (c === '}')
61
- d--;
62
- }
63
- out += '(?:' + parts.map(maskBody).join('|') + ')';
64
- i = j - 1;
65
- }
66
- else if (ch === '*') {
67
- out += '[^.]+(?:\\.[^.]+)*';
68
- }
69
- else if ('.\\+?()[]^$|'.includes(ch)) {
70
- out += '\\' + ch;
71
- }
72
- else {
73
- out += ch;
74
- }
75
- }
76
- // END_BLOCK_MASK_COMPILE
77
- return out;
78
- }
79
- const regexCache = new Map();
80
- // START_CONTRACT: matchMask
81
- // PURPOSE: Сопоставить значение с маской, кэшируя скомпилированный RegExp.
82
- // INPUTS: { mask: string; value: string }
83
- // OUTPUTS: { boolean }
84
- // SIDE_EFFECTS: заполняет regexCache
85
- // LINKS: M-ACL, V-M-ACL
86
- // END_CONTRACT: matchMask
87
- export function matchMask(mask, value) {
88
- let re = regexCache.get(mask);
89
- if (!re) {
90
- re = new RegExp(`^${maskBody(mask)}$`);
91
- regexCache.set(mask, re);
92
- }
93
- return re.test(value);
94
- }
95
- // --- резолюция ----------------------------------------------------------------
96
- /** Все имена, под которыми класс виден правилам: id+алиас его самого и каждого предка. */
97
- function classNames(cls, registry) {
98
- const names = [];
99
- for (const id of cls.ancestors) {
100
- names.push(id);
101
- try {
102
- const a = registry.resolve(id).alias;
103
- if (a && a !== id)
104
- names.push(a);
105
- }
106
- catch {
107
- /* предок вне реестра — пропуск */
108
- }
109
- }
110
- return names;
111
- }
112
- const matchesApi = (r, endpoint) => {
113
- if (r.category !== 'API')
114
- return false;
115
- if (r.pattern == null)
116
- return true;
117
- const m = r.pattern.endpoint;
118
- return typeof m === 'string' && matchMask(m, endpoint);
119
- };
120
- /** Ресурс операции op применим к классу? (class-ключ — маска с lineage; нет ключа — все). */
121
- const matchesRow = (r, op, names) => {
122
- if (r.category !== op)
123
- return false;
124
- if (r.pattern == null)
125
- return true;
126
- const clsMask = r.pattern.class;
127
- return typeof clsMask !== 'string' || names.some((n) => matchMask(clsMask, n));
128
- };
129
- /** "$account" → id субъекта, рекурсивно по значениям шаблона. */
130
- function substitute(v, account) {
131
- if (v === '$account')
132
- return account;
133
- if (Array.isArray(v))
134
- return v.map((x) => substitute(x, account));
135
- if (v && typeof v === 'object') {
136
- return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, substitute(x, account)]));
137
- }
138
- return v;
139
- }
140
- /** Остаточный шаблон строки победившего allow: все ключи, кроме class, с подстановкой. */
141
- function rowFilter(r, account) {
142
- if (r.pattern == null)
143
- return undefined;
144
- const rest = Object.entries(r.pattern).filter(([k]) => k !== 'class');
145
- if (!rest.length)
146
- return undefined;
147
- return Object.fromEntries(rest.map(([k, v]) => [k, substitute(v, account)]));
148
- }
149
- // START_CONTRACT: decide
150
- // PURPOSE: Выбрать РОВНО ОДНО победившее правило: max weight; при равенстве deny бьёт allow, равные allow — по alias ресурса.
151
- // INPUTS: { rules: Rule[]; subjects: Set<string> - alias'ы субъекта; objects: Map<string,Resource> - применимые ресурсы }
152
- // OUTPUTS: { { winner?: Rule } }
153
- // SIDE_EFFECTS: none
154
- // LINKS: M-ACL, V-M-ACL
155
- // END_CONTRACT: decide
156
- /** Победитель: max weight; равный вес — deny бьёт allow, равные allow — по alias объекта. */
157
- function decide(rules, subjects, objects) {
158
- // START_BLOCK_DECIDE_WINNER
159
- let winner;
160
- for (const rule of rules) {
161
- if (!rule.enabled || !subjects.has(rule.account) || !objects.has(rule.resource))
162
- continue;
163
- if (!winner) {
164
- winner = rule;
165
- continue;
166
- }
167
- const w = rule.weight ?? 0;
168
- const ww = winner.weight ?? 0;
169
- if (w > ww ||
170
- (w === ww && winner.permission === 'allow' && rule.permission !== 'allow') ||
171
- (w === ww && winner.permission === rule.permission && rule.resource < winner.resource)) {
172
- winner = rule;
173
- }
174
- }
175
- // END_BLOCK_DECIDE_WINNER
176
- return { winner };
177
- }
178
- // START_CONTRACT: toDecision
179
- // PURPOSE: Превратить победителя в AclDecision (allow + остаточный row-фильтр | deny + code/message | deny-by-default).
180
- // INPUTS: { winner?: Rule; objects: Map<string,Resource>; account: string }
181
- // OUTPUTS: { AclDecision }
182
- // SIDE_EFFECTS: none
183
- // LINKS: M-ACL, V-M-ACL, type-AclDecision
184
- // END_CONTRACT: toDecision
185
- function toDecision(winner, objects, account) {
186
- if (!winner)
187
- return { allow: false, message: 'no matching rule (deny by default)' };
188
- if (winner.permission !== 'allow') {
189
- const meta = (winner.meta ?? {});
190
- return { allow: false, rule: winner, code: meta.code, message: meta.message };
191
- }
192
- const res = objects.get(winner.resource);
193
- // остаточный шаблон строк — только у ресурсов-операций (у API pattern — не строки Entity)
194
- const filter = ACL_OPS.includes(res.category) ? rowFilter(res, account) : undefined;
195
- return filter ? { allow: true, rule: winner, filter } : { allow: true, rule: winner };
196
- }
197
- const subjectAliases = (resources, cats) => new Set(resources.filter((r) => r.category === 'ACCOUNT' && matchCategories(r.pattern, cats)).map((r) => r.alias));
198
- const toMap = (rs) => new Map(rs.map((r) => [r.alias, r]));
199
- const accId = (a) => (typeof a === 'object' ? a.id : a);
200
- // START_CONTRACT: makeAcl
201
- // PURPOSE: Построить рантайм-фасад AclApi (check по эндпоинту / checkData по классу+op / reload) с кэшом Resource/Rule.
202
- // INPUTS: { tables: Tables; registry: Registry }
203
- // OUTPUTS: { AclApi }
204
- // SIDE_EFFECTS: лениво читает resources/rules через tables (кэш до reload())
205
- // LINKS: M-ACL, V-M-ACL
206
- // END_CONTRACT: makeAcl
207
- export function makeAcl(tables, registry) {
208
- let cache = null;
209
- const load = () => (cache ??= (async () => ({
210
- resources: await tables.resources.find(),
211
- rules: await tables.rules.find({ enabled: true }),
212
- }))());
213
- const catsOf = async (account) => (await tables.accounts.get(accId(account)))?.categories ?? [];
214
- return {
215
- async check(account, endpoint) {
216
- const [{ resources, rules }, cats] = await Promise.all([load(), catsOf(account)]);
217
- const objects = toMap(resources.filter((r) => matchesApi(r, endpoint)));
218
- const { winner } = decide(rules, subjectAliases(resources, cats), objects);
219
- return toDecision(winner, objects, accId(account));
220
- },
221
- async checkData(account, className, op) {
222
- const [{ resources, rules }, cats] = await Promise.all([load(), catsOf(account)]);
223
- const names = classNames(registry.resolve(className), registry);
224
- const objects = toMap(resources.filter((r) => matchesRow(r, op, names)));
225
- const { winner } = decide(rules, subjectAliases(resources, cats), objects);
226
- return toDecision(winner, objects, accId(account));
1
+ /**
2
+ * db.acl — решения прав для приложения (план, §2.8, этап 5, п. 7). Сами права применяет база
3
+ * (политики RLS по плану прав, 50-acl.sql); эти вызовы отвечают «можно ли» заранее — для интерфейса
4
+ * и эндпоинтов приложения. Правила — строки Resource и rule (группа → объект, allow | deny, вес);
5
+ * решение — ровно одно правило с наибольшим весом, при равном весе запрет сильнее, нет правил — запрет.
6
+ */
7
+ import { removed } from './errors.js';
8
+ export function makeAcl(S, exec) {
9
+ const api = {
10
+ async check(endpoint) {
11
+ const [r] = await exec.run((q) => q.unsafe(`select ${S}.acl_check($1) as d`, [endpoint]), { read: true });
12
+ return r.d;
227
13
  },
228
- reload() {
229
- cache = null;
14
+ async checkData(className, op) {
15
+ const [r] = await exec.run((q) => q.unsafe(`select ${S}.acl_check_data($1, $2) as d`, [className, op]), { read: true });
16
+ return r.d;
230
17
  },
231
18
  };
232
- }
233
- // --- enforce-компилятор (connect({enforceAcl: true})) ----------------------------
234
- // START_CONTRACT: compileEnforcer
235
- // PURPOSE: Скомпилировать синхронный энфорсер (ClassDef, op) → AclDecision с мемоизацией — субъект и правила снимаются В МОМЕНТ вызова (на connect либо на db.reloadSchema()).
236
- // INPUTS: { src: AclSource; account: string; cats: string[]; registry: Registry }
237
- // OUTPUTS: { (cls: ClassDef, op: AclOp) => AclDecision }
238
- // SIDE_EFFECTS: none (мемо-кэш решений)
239
- // LINKS: M-ACL, V-M-ACL, M-SQL, M-WRITE
240
- // END_CONTRACT: compileEnforcer
241
- /** Синхронный резолвер для цепочек: категории субъекта и правила — снимок на момент
242
- * компиляции (connect либо db.reloadSchema(), который пересобирает энфорсер). */
243
- export function compileEnforcer(src, account, cats, registry) {
244
- // START_BLOCK_ENFORCER_MEMO
245
- const subjects = subjectAliases(src.resources, cats);
246
- const memo = new Map();
247
- return (cls, op) => {
248
- const key = `${cls.id} ${op}`;
249
- let d = memo.get(key);
250
- if (!d) {
251
- const names = classNames(cls, registry);
252
- const objects = toMap(src.resources.filter((r) => matchesRow(r, op, names)));
253
- const { winner } = decide(src.rules, subjects, objects);
254
- d = toDecision(winner, objects, account);
255
- memo.set(key, d);
256
- }
257
- return d;
258
- };
259
- }
260
- // END_BLOCK_ENFORCER_MEMO
261
- // START_CONTRACT: aclDenied
262
- // PURPOSE: Собрать читаемую Error отказа ACL для цепочек (её бросают вызывающие sql/write).
263
- // INPUTS: { op: AclOp; clsId: string; d: AclDecision }
264
- // OUTPUTS: { Error }
265
- // SIDE_EFFECTS: none (возвращает Error, не бросает)
266
- // LINKS: M-ACL, V-M-ACL
267
- // END_CONTRACT: aclDenied
268
- /** Читаемая ошибка отказа для цепочек. */
269
- export function aclDenied(op, clsId, d) {
270
- return new Error(`letopis: acl denies ${op} on ${clsId}${d.message ? ` — ${d.message}` : ''}`);
19
+ return new Proxy(api, {
20
+ get(t, p, r) {
21
+ if (p === 'reload')
22
+ throw removed('db.acl.reload()', 'права обновляются сами: база применяет действующие правила');
23
+ return Reflect.get(t, p, r);
24
+ },
25
+ });
271
26
  }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Эксплуатация (план, этап 6, п. 3, 4, 6–8, 12): verify, maintain и его запуск по расписанию, reset,
3
+ * обрезка истории, новый ключ класса, describe. Всё проверяет база: функции владельца 90-time.sql
4
+ * сами сверяют права (letopis.* — системный администратор или администраторское подключение).
5
+ */
6
+ import type postgres from 'postgres';
7
+ import type { Registry } from './registry.js';
8
+ import { type Executor, type TxContext } from './tx.js';
9
+ type Sql = postgres.Sql<Record<string, unknown>>;
10
+ export interface VerifyIssue {
11
+ id: string;
12
+ rev?: number;
13
+ class?: string;
14
+ problem: string;
15
+ [k: string]: unknown;
16
+ }
17
+ export interface VerifyResult {
18
+ ok: boolean;
19
+ issues: VerifyIssue[];
20
+ /** Хэш всех голов цепочек — храните вне базы: подмену последних версий выдаст его расхождение. */
21
+ anchor: string;
22
+ }
23
+ export interface MaintainOptions {
24
+ /** Момент «сейчас» для прореживания — только в установке с allowReset (тесты). */
25
+ now?: string | Date;
26
+ /** Пачек прореживания за вызов (по умолчанию 100); каждая — своя транзакция. */
27
+ batches?: number;
28
+ /** Версий в пачке (по умолчанию 10 000). */
29
+ limit?: number;
30
+ }
31
+ export interface MaintainReport {
32
+ /** Другой процесс уже обслуживает установку (advisory-блокировка): этот вызов ничего не делал. */
33
+ skipped: boolean;
34
+ /** Удалено истёкших и погашенных сессий и одноразовых кодов. */
35
+ sessions: number;
36
+ codes: number;
37
+ /** Прорежено версий журнала. */
38
+ thinned: number;
39
+ batches: number;
40
+ /** Прореживание не дошло до конца журнала — следующий вызов продолжит. */
41
+ more: boolean;
42
+ /** Заполнение очереди сигналов PostgreSQL (0…1); больше половины — предупреждение. */
43
+ queue: number;
44
+ }
45
+ export interface ResetOptions {
46
+ level: 'sessions' | 'tenant' | 'all';
47
+ /** Подтверждение — имя схемы установки. */
48
+ confirm: string;
49
+ /** level: 'tenant' — арендатор (по умолчанию — арендатор сессии). */
50
+ tenant?: string;
51
+ }
52
+ export interface ResetResult {
53
+ level: string;
54
+ sessions?: number;
55
+ tenant?: string;
56
+ rows?: number;
57
+ versions?: number;
58
+ /** level: 'all' — новый ключ сервиса: прежние сессии и ключи больше не действуют. */
59
+ serviceKey?: string;
60
+ }
61
+ /** Машинное описание модели (db.describe()). */
62
+ export interface ModelDescription {
63
+ schema: string;
64
+ tenant: string | null;
65
+ classes: {
66
+ name: string;
67
+ kind: 'hub' | 'link';
68
+ alias?: string;
69
+ abstract: boolean;
70
+ parent?: string;
71
+ ancestors: string[];
72
+ system: boolean;
73
+ fields: Record<string, unknown>;
74
+ required: string[];
75
+ ends: Record<string, unknown>;
76
+ key: string[];
77
+ history: unknown;
78
+ ownerDefault: string;
79
+ cache?: {
80
+ ttl?: number;
81
+ };
82
+ meta: Record<string, unknown>;
83
+ }[];
84
+ methods: {
85
+ chain: readonly string[];
86
+ db: readonly string[];
87
+ };
88
+ }
89
+ export interface AdminCtx {
90
+ sql: Sql;
91
+ schema: string;
92
+ /** Схема в кавычках. */
93
+ S: string;
94
+ txc: TxContext;
95
+ exec: Executor;
96
+ registry: () => Registry;
97
+ tenant: () => string | null;
98
+ }
99
+ export declare function verify(a: AdminCtx, opts?: {
100
+ data?: boolean;
101
+ }): Promise<VerifyResult>;
102
+ /**
103
+ * Обслуживание: истёкшие сессии и коды, прореживание журнала пачками (каждая — своя транзакция,
104
+ * долгих блокировок нет), заполнение очереди сигналов. Из нескольких процессов работает один: пачка
105
+ * первым делом берёт транзакционную advisory-блокировку (pg_try_advisory_xact_lock), и та снимается
106
+ * с фиксацией пачки. Сеансовая блокировка за пулом в режиме транзакций (PgBouncer) оставалась бы на
107
+ * серверном соединении, которое пул отдаёт другим клиентам, — и обслуживание молча вставало бы
108
+ * (skipped у всех). Блокировку держит другой процесс: до первой пачки — skipped; между пачками —
109
+ * вызов останавливается (продолжит тот процесс), отчёт — о сделанном, more: true.
110
+ */
111
+ export declare function maintain(a: AdminCtx, opts?: MaintainOptions): Promise<MaintainReport>;
112
+ /** Предупреждение о заполненной очереди сигналов (§2.12): больше половины — зависший слушатель. */
113
+ export declare function queueWarning(queue: number): string | null;
114
+ /** Интервал расписания: число мс или '30s', '15m', '1h', '1d'. */
115
+ export declare function parseInterval(v: string | number): number;
116
+ /** Запуск maintain() по расписанию (connect({ maintain: '1h' })): ошибки — предупреждением. */
117
+ export declare function scheduleMaintain(run: () => Promise<unknown>, every: string | number): () => void;
118
+ /**
119
+ * Сброс (этап 6, п. 6). Предохранители — в базе: флаг allowReset установки, право letopis.reset,
120
+ * подтверждение именем схемы. tenant — пачками (длинная операция не держит горизонт подписок).
121
+ */
122
+ export declare function reset(a: AdminCtx, opts: ResetOptions): Promise<ResetResult>;
123
+ /**
124
+ * Обрезка истории (этап 6, п. 3): версии строже момента before удаляются, текущая — никогда; на место
125
+ * версии 1 встаёт запись trim. target — имя класса (с потомками), id или список id, строка ответа.
126
+ */
127
+ export declare function trimHistory(a: AdminCtx, target: string | string[] | {
128
+ id: string;
129
+ } | {
130
+ id: string;
131
+ }[], before: string | Date): Promise<number>;
132
+ /** Новый ключ класса (этап 6, п. 7): строки семейства переносятся на новые id. Возвращает их число. */
133
+ export declare function rekeyClass(a: AdminCtx, cls: string, key: string[]): Promise<number>;
134
+ /** Машинное описание модели арендатора вместе с метаданными классов (этап 6, п. 12). */
135
+ export declare function describe(a: AdminCtx): ModelDescription;
136
+ /** Описание модели текстом для языковых моделей (llms.txt). */
137
+ export declare function describeText(d: ModelDescription): string;
138
+ export {};