letopis 1.4.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/AGENT-CHEATSHEET.md +516 -398
  2. package/CHANGELOG.md +739 -0
  3. package/LICENSE +75 -21
  4. package/LICENSE-COMMERCIAL.md +49 -0
  5. package/README.md +1312 -1190
  6. package/api-contract.json +381 -0
  7. package/dist/acl.d.ts +81 -1
  8. package/dist/acl.js +281 -13
  9. package/dist/admin.d.ts +11 -141
  10. package/dist/admin.js +28 -186
  11. package/dist/auth.d.ts +20 -5
  12. package/dist/auth.js +80 -21
  13. package/dist/cache.d.ts +8 -1
  14. package/dist/cache.js +30 -8
  15. package/dist/chain.d.ts +46 -48
  16. package/dist/chain.js +556 -223
  17. package/dist/cli.js +73 -96
  18. package/dist/demo/booking.d.ts +352 -36
  19. package/dist/describe.d.ts +47 -0
  20. package/dist/describe.js +195 -0
  21. package/dist/errors.d.ts +77 -13
  22. package/dist/errors.js +231 -30
  23. package/dist/index.d.ts +62 -74
  24. package/dist/index.js +312 -155
  25. package/dist/internal.d.ts +7 -0
  26. package/dist/jsonschema.js +18 -17
  27. package/dist/load.d.ts +29 -17
  28. package/dist/load.js +235 -190
  29. package/dist/log.d.ts +121 -0
  30. package/dist/log.js +633 -0
  31. package/dist/mcp.d.ts +5 -7
  32. package/dist/mcp.js +194 -225
  33. package/dist/messages/access.d.ts +90 -0
  34. package/dist/messages/access.js +113 -0
  35. package/dist/messages/api.d.ts +237 -0
  36. package/dist/messages/api.js +304 -0
  37. package/dist/messages/boot.d.ts +201 -0
  38. package/dist/messages/boot.js +256 -0
  39. package/dist/messages/codes.d.ts +172 -0
  40. package/dist/messages/codes.js +173 -0
  41. package/dist/messages/index.d.ts +98 -0
  42. package/dist/messages/index.js +121 -0
  43. package/dist/messages/log.d.ts +229 -0
  44. package/dist/messages/log.js +102 -0
  45. package/dist/messages/mcp.d.ts +306 -0
  46. package/dist/messages/mcp.js +387 -0
  47. package/dist/messages/model.d.ts +256 -0
  48. package/dist/messages/model.js +314 -0
  49. package/dist/messages/query.d.ts +141 -0
  50. package/dist/messages/query.js +171 -0
  51. package/dist/messages/sql-access.d.ts +70 -0
  52. package/dist/messages/sql-access.js +87 -0
  53. package/dist/messages/sql-core.d.ts +164 -0
  54. package/dist/messages/sql-core.js +198 -0
  55. package/dist/messages/sql-time.d.ts +86 -0
  56. package/dist/messages/sql-time.js +111 -0
  57. package/dist/messages/sql-write.d.ts +246 -0
  58. package/dist/messages/sql-write.js +270 -0
  59. package/dist/messages/time.d.ts +23 -0
  60. package/dist/messages/time.js +29 -0
  61. package/dist/messages/types.d.ts +27 -0
  62. package/dist/messages/types.js +5 -0
  63. package/dist/messages/write.d.ts +452 -0
  64. package/dist/messages/write.js +572 -0
  65. package/dist/model.d.ts +77 -60
  66. package/dist/model.js +43 -34
  67. package/dist/ops.d.ts +35 -0
  68. package/dist/ops.js +101 -0
  69. package/dist/pglite.d.ts +0 -9
  70. package/dist/pglite.js +3 -1
  71. package/dist/registry.d.ts +3 -19
  72. package/dist/registry.js +3 -16
  73. package/dist/signals.d.ts +1 -5
  74. package/dist/signals.js +1 -5
  75. package/dist/sql.d.ts +9 -1
  76. package/dist/sql.js +175 -107
  77. package/dist/sync.d.ts +10 -4
  78. package/dist/sync.js +90 -17
  79. package/dist/tx.d.ts +21 -2
  80. package/dist/tx.js +76 -13
  81. package/dist/typed.d.ts +252 -53
  82. package/dist/types.d.ts +93 -90
  83. package/dist/types.js +17 -14
  84. package/dist/up.d.ts +56 -35
  85. package/dist/up.js +229 -83
  86. package/dist/uuid.d.ts +8 -0
  87. package/dist/uuid.js +14 -6
  88. package/dist/validate.d.ts +13 -1
  89. package/dist/validate.js +82 -33
  90. package/dist/watch.d.ts +10 -16
  91. package/dist/watch.js +36 -77
  92. package/dist/write.d.ts +87 -17
  93. package/dist/write.js +441 -206
  94. package/llms.txt +48 -31
  95. package/package.json +13 -14
  96. package/sql/10-core.sql +7 -50
  97. package/sql/15-errors.sql +64 -10
  98. package/sql/20-context.sql +18 -9
  99. package/sql/30-validate.sql +79 -30
  100. package/sql/40-class.sql +41 -115
  101. package/sql/50-acl.sql +8 -13
  102. package/sql/60-write.sql +220 -676
  103. package/sql/70-read.sql +11 -13
  104. package/sql/80-auth.sql +109 -42
  105. package/sql/90-time.sql +108 -947
  106. package/sql/95-seed.system.sql +7 -14
  107. package/sql/99-revision.sql +6 -1
  108. package/sql/README.md +32 -24
  109. package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
  110. package/sql/migrate/0010-impersonation-no-account.sql +5 -0
  111. package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
  112. package/sql/migrate/README.md +18 -0
  113. package/AGENT-CHEATSHEET.en.md +0 -488
  114. package/MIGRATION.md +0 -190
  115. package/README.en.md +0 -2910
  116. package/dist/import.d.ts +0 -185
  117. package/dist/import.js +0 -820
  118. package/dist/project.d.ts +0 -359
  119. package/dist/project.js +0 -1258
  120. package/sql/75-history.sql +0 -436
  121. package/sql/88-derived.sql +0 -1236
  122. package/sql/migrate/0006-changed-paths.sql +0 -3
  123. package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/dist/model.d.ts CHANGED
@@ -17,18 +17,20 @@ export type OnDelete = 'restrict' | 'cascade' | 'unset';
17
17
  export interface ClassMeta {
18
18
  [key: string]: unknown;
19
19
  }
20
- /** Политика хранения истории (§2.11). */
21
- export interface HistoryPolicy {
22
- all: string;
23
- daily?: string;
24
- weekly?: string;
25
- monthly?: string;
26
- yearly?: string;
27
- tz?: string;
28
- }
29
20
  type Target = AnyModel | string;
30
- /** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. */
31
- export declare class End<Many extends boolean = boolean, Required extends boolean = boolean> {
21
+ /** Имя класса цели: у модели — её имя, у строки — сама строка. */
22
+ type TargetName<T> = T extends string ? T : T extends {
23
+ readonly name: infer N extends string;
24
+ } ? N : string;
25
+ /**
26
+ * Обязателен ли множественный конец по `min` (для типов): `min` больше нуля — да, нет `min` или ноль — нет, число,
27
+ * известное только во время выполнения, — неизвестно (`boolean`).
28
+ */
29
+ type MinRequired<O> = O extends {
30
+ min: infer N;
31
+ } ? (N extends 0 | undefined ? false : number extends N ? boolean : true) : false;
32
+ /** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. `T` — имена классов целей (только для типов). */
33
+ export declare class End<Many extends boolean = boolean, Required extends boolean = boolean, T extends string = string> {
32
34
  readonly targets: readonly Target[];
33
35
  readonly many: Many;
34
36
  readonly required: Required;
@@ -36,6 +38,8 @@ export declare class End<Many extends boolean = boolean, Required extends boolea
36
38
  readonly max?: number;
37
39
  readonly onDeleteRule?: OnDelete;
38
40
  readonly metaData?: Record<string, unknown>;
41
+ /** Только для вывода типов: имена классов целей — строгие переходы и слоты цепочек (план 2.0, этап 4, п. 1). */
42
+ readonly __targets?: T;
39
43
  constructor(targets: readonly Target[], opts: {
40
44
  many: Many;
41
45
  required: Required;
@@ -46,22 +50,25 @@ export declare class End<Many extends boolean = boolean, Required extends boolea
46
50
  });
47
51
  private with;
48
52
  /** Необязательный конец. */
49
- optional(): End<Many, false>;
53
+ optional(): End<Many, false, T>;
50
54
  /** Правило при удалении цели: restrict (по умолчанию), cascade, unset. */
51
- onDelete(rule: OnDelete): End<Many, Required>;
55
+ onDelete(rule: OnDelete): End<Many, Required, T>;
52
56
  /** Метаданные конца (§2.13). */
53
- meta(meta: Record<string, unknown>): End<Many, Required>;
57
+ meta(meta: Record<string, unknown>): End<Many, Required, T>;
54
58
  /** Имена классов целей. */
55
59
  classNames(): string[];
56
60
  }
57
61
  /** Одиночный конец; несколько классов — союз: `one(Service, Product)`. По умолчанию обязательный. */
58
- export declare function one(...targets: Target[]): End<false, true>;
59
- /** Множественный конец: список целей в одном конце (§4 предложения). */
60
- export declare function many(target: Target | readonly Target[], opts?: {
62
+ export declare function one<const T extends readonly Target[]>(...targets: T): End<false, true, TargetName<T[number]>>;
63
+ /**
64
+ * Множественный конец: список целей в одном конце (§4 предложения). Обязателен при `min` больше нуля — `unset()` у него
65
+ * в цепочке типизированного подключения нет.
66
+ */
67
+ export declare function many<const T extends Target | readonly Target[], const O extends {
61
68
  min?: number;
62
69
  max?: number;
63
70
  onDelete?: OnDelete;
64
- }): End<true, boolean>;
71
+ } = Record<never, never>>(target: T, opts?: O): End<true, MinRequired<O>, TargetName<T extends readonly (infer X)[] ? X : T>>;
65
72
  type EndsInput = Record<string, End | AnyModel>;
66
73
  type EndOf<E> = E extends End<infer M, infer R> ? End<M, R> : End<false, true>;
67
74
  type LinkValue<E> = EndOf<E> extends End<true, boolean> ? string[] : string;
@@ -79,39 +86,14 @@ export interface ClassDef<S extends z.ZodRawShape = z.ZodRawShape, E extends End
79
86
  fields?: z.ZodObject<S> | S;
80
87
  key?: readonly string[];
81
88
  onDelete?: Partial<Record<keyof E & string, OnDelete>>;
82
- history?: boolean | HistoryPolicy;
89
+ /** Вести ли журнал версий (по умолчанию да). */
90
+ history?: boolean;
83
91
  ownerDefault?: 'tenant' | 'actor';
84
92
  cache?: {
85
93
  ttl?: number;
86
94
  };
87
95
  meta?: ClassMeta;
88
- /** Производный класс (план 1.1, §2.4): строки пишет только проектор с этим именем; наследуется. */
89
- derivedBy?: string;
90
- /**
91
- * Агрегаты в базе (план 1.1, §2.4): поле — сумма поля (`sum`) или число (`count: true`) живых строк класса `of` того
92
- * же арендатора, чей одиночный конец `via` ведёт на строку; `where` — JSON Schema над данными строки-источника.
93
- * Поле считает база, приложение его не пишет. Наследуется, потомок заменяет целиком.
94
- */
95
- aggregate?: Record<string, AggregateDef>;
96
96
  }
97
- /** Описание одного агрегатного поля: `sum` или `count: true`; `of` — класс-источник (имя или модель). */
98
- export type AggregateDef = {
99
- sum: string;
100
- count?: never;
101
- of: string | {
102
- readonly name: string;
103
- };
104
- via: string;
105
- where?: JsonSchema;
106
- } | {
107
- count: true;
108
- sum?: never;
109
- of: string | {
110
- readonly name: string;
111
- };
112
- via: string;
113
- where?: JsonSchema;
114
- };
115
97
  /** Описание класса в строке `Class` (метасхема `class_metaschema()` в lib/sql/40-class.sql). */
116
98
  export interface ClassDescription {
117
99
  name: string;
@@ -133,20 +115,12 @@ export interface ClassDescription {
133
115
  meta?: Record<string, unknown>;
134
116
  }>;
135
117
  key?: string[];
136
- history?: boolean | HistoryPolicy;
118
+ history?: boolean;
137
119
  ownerDefault?: 'tenant' | 'actor';
138
120
  cache?: {
139
121
  ttl?: number;
140
122
  };
141
123
  meta?: ClassMeta;
142
- derivedBy?: string;
143
- aggregate?: Record<string, {
144
- sum?: string;
145
- count?: true;
146
- of: string;
147
- via: string;
148
- where?: JsonSchema;
149
- }>;
150
124
  }
151
125
  /** Правило refine, которое не попадёт в базу: проверяется только в библиотеке. */
152
126
  export interface LostRule {
@@ -155,7 +129,36 @@ export interface LostRule {
155
129
  }
156
130
  type DataOf<P, S extends z.ZodRawShape> = P extends Model<string, infer PD, unknown> ? Omit<PD, keyof z.output<z.ZodObject<S>>> & z.output<z.ZodObject<S>> : z.output<z.ZodObject<S>>;
157
131
  type AllLinks<P, E extends EndsInput> = P extends Model<string, unknown, infer PL> ? Omit<PL, keyof E> & LinksOf<E> : LinksOf<E>;
158
- export interface Model<N extends string = string, D = unknown, L = unknown, A extends string = string> {
132
+ /**
133
+ * Концы класса для строгих типов цепочек (план 2.0, этап 4, п. 1): роль → классы целей, множественный ли, обязательный
134
+ * ли; с концами предков (потомок заменяет конец целиком). Тот же вид выдаёт `letopis types` (generateTypes, sync.ts).
135
+ */
136
+ type EndSpecOf<V> = V extends End<infer Mn, infer R, infer T> ? {
137
+ targets: T;
138
+ many: Mn;
139
+ required: R;
140
+ } : V extends {
141
+ readonly name: infer N extends string;
142
+ } ? {
143
+ targets: N;
144
+ many: false;
145
+ required: true;
146
+ } : never;
147
+ type EndsSpecOf<E> = {
148
+ [K in keyof E]: EndSpecOf<E[K]>;
149
+ };
150
+ type AllEnds<P, E extends EndsInput> = P extends Model<string, unknown, unknown, string, infer PE, string> ? Omit<PE, keyof E> & EndsSpecOf<E> : EndsSpecOf<E>;
151
+ /** Имя класса и его предков — как `ancestors` реестра (семейства и переходы строгих типов). */
152
+ type Lineage<N extends string, P> = N | (P extends Model<infer PN, unknown, unknown, string, unknown, infer PP> ? PN | PP : never);
153
+ /** Концы описания класса: `ends` или пусто. */
154
+ type EndsIn<D> = D extends {
155
+ ends?: infer E extends EndsInput;
156
+ } ? E : Record<never, never>;
157
+ /**
158
+ * Модель класса. `N` — имя, `D` — данные строки, `L` — концы строки, `A` — алиас, `E` — концы класса (роль → классы
159
+ * целей, множественный ли, обязательный ли), `P` — имя класса и его предков: всё это — для строгих типов цепочек.
160
+ */
161
+ export interface Model<N extends string = string, D = unknown, L = unknown, A extends string = string, E = unknown, P extends string = N> {
159
162
  readonly name: N;
160
163
  readonly kind: 'hub' | 'link';
161
164
  readonly alias?: A;
@@ -180,9 +183,11 @@ export interface Model<N extends string = string, D = unknown, L = unknown, A ex
180
183
  readonly __types?: {
181
184
  data: D;
182
185
  links: L;
186
+ ends: E;
187
+ ancestors: P;
183
188
  };
184
189
  }
185
- export type AnyModel = Model<string, any, any, any>;
190
+ export type AnyModel = Model<string, any, any, any, any, any>;
186
191
  /** Тип данных строки класса: `Infer<typeof Master>`. */
187
192
  export type Infer<M> = M extends Model<string, infer D, unknown> ? D : never;
188
193
  /** Тип концов строки класса. */
@@ -191,13 +196,25 @@ type ShapeOf<F> = F extends z.ZodObject<infer S> ? S : F extends z.ZodRawShape ?
191
196
  type AliasOf<D> = D extends {
192
197
  alias: infer A extends string;
193
198
  } ? A : string;
199
+ /** Предок из описания класса (`extends`); нет — unknown. */
200
+ type ExtendsOf<D> = D extends {
201
+ extends?: infer P;
202
+ } ? P : unknown;
203
+ /** Описание класса без полей — третьим аргументом короткой формы. */
204
+ type ClassOpts = Omit<ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>, 'fields'>;
205
+ /** Тип модели по имени, описанию и собственным полям: данные и концы — с предками. */
206
+ type ModelOf<N extends string, D, S extends z.ZodRawShape> = Model<N, DataOf<ExtendsOf<D>, S>, AllLinks<ExtendsOf<D>, EndsIn<D>>, AliasOf<D>, AllEnds<ExtendsOf<D>, EndsIn<D>>, Lineage<N, ExtendsOf<D>>>;
194
207
  /**
195
208
  * Описывает класс объектов (хаб): поля, ключ, концы и свойства класса. Класс-связь объектов со своими данными
196
209
  * описывает `link()`.
197
210
  */
198
- export declare function hub<N extends string, S extends z.ZodRawShape>(name: N, shape: S, opts: Omit<ClassDef, 'fields'>): Model<N, z.output<z.ZodObject<S>>, Record<never, never>>;
199
- export declare function hub<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D): Model<N, DataOf<D['extends'], ShapeOf<D['fields']>>, AllLinks<D['extends'], D['ends'] extends EndsInput ? D['ends'] : Record<never, never>>, AliasOf<D>>;
211
+ export declare function hub<N extends string, S extends z.ZodRawShape, const O extends ClassOpts>(name: N, shape: S, opts: O): ModelOf<N, O, S>;
212
+ export declare function hub<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D): ModelOf<N, D, ShapeOf<D['fields']>>;
213
+ /** Короткая форма без опций: hub('Имя', { поле: z… }) — поля класса (полная форма { fields: z.object(…) } — выше). */
214
+ export declare function hub<N extends string, S extends z.ZodRawShape>(name: N, shape: S): ModelOf<N, {}, S>;
200
215
  /** Связь — отношение между объектами со своими данными (бронь, навык). */
201
- export declare function link<N extends string, S extends z.ZodRawShape>(name: N, shape: S, opts: Omit<ClassDef, 'fields'>): Model<N, z.output<z.ZodObject<S>>, Record<never, never>>;
202
- export declare function link<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D): Model<N, DataOf<D['extends'], ShapeOf<D['fields']>>, AllLinks<D['extends'], D['ends'] extends EndsInput ? D['ends'] : Record<never, never>>, AliasOf<D>>;
216
+ export declare function link<N extends string, S extends z.ZodRawShape, const O extends ClassOpts>(name: N, shape: S, opts: O): ModelOf<N, O, S>;
217
+ export declare function link<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D): ModelOf<N, D, ShapeOf<D['fields']>>;
218
+ /** Короткая форма без опций: link('Имя', { поле: z… }) — поля класса (полная форма { fields: z.object(…) } — выше). */
219
+ export declare function link<N extends string, S extends z.ZodRawShape>(name: N, shape: S): ModelOf<N, {}, S>;
203
220
  export { normalizeSchema, translatePattern, type JsonSchema } from './jsonschema.js';
package/dist/model.js CHANGED
@@ -11,9 +11,10 @@
11
11
  * Поля, концы и ключ наследуются по правилу 0.21 «потомок заменяет поле целиком» (`extends`).
12
12
  */
13
13
  import { z } from 'zod';
14
- import { LetopisError } from './errors.js';
14
+ import { lerr } from './errors.js';
15
15
  import { normalizeSchema } from './jsonschema.js';
16
- /** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. */
16
+ import { RESERVED_CLASS_NAMES } from './types.js';
17
+ /** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. `T` — имена классов целей (только для типов). */
17
18
  export class End {
18
19
  targets;
19
20
  many;
@@ -24,7 +25,7 @@ export class End {
24
25
  metaData;
25
26
  constructor(targets, opts) {
26
27
  if (!targets.length)
27
- throw new LetopisError('invalid_class', 'у конца должен быть хотя бы один класс цели');
28
+ throw lerr('model.end_no_targets');
28
29
  this.targets = targets;
29
30
  this.many = opts.many;
30
31
  this.required = opts.required;
@@ -58,25 +59,31 @@ export class End {
58
59
  export function one(...targets) {
59
60
  return new End(targets, { many: false, required: true });
60
61
  }
61
- /** Множественный конец: список целей в одном конце (§4 предложения). */
62
+ /**
63
+ * Множественный конец: список целей в одном конце (§4 предложения). Обязателен при `min` больше нуля — `unset()` у него
64
+ * в цепочке типизированного подключения нет.
65
+ */
62
66
  export function many(target, opts = {}) {
63
67
  const targets = Array.isArray(target) ? target : [target];
64
68
  return new End(targets, { many: true, required: (opts.min ?? 0) > 0, min: opts.min, max: opts.max, onDelete: opts.onDelete });
65
69
  }
66
70
  const NAME = /^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$/;
67
71
  /** Ключи полной формы описания класса (ClassDef). */
68
- const DEF_KEYS = new Set(['alias', 'extends', 'abstract', 'ends', 'fields', 'key', 'onDelete', 'history', 'ownerDefault', 'cache', 'meta', 'derivedBy', 'aggregate']);
72
+ const DEF_KEYS = new Set(['alias', 'extends', 'abstract', 'ends', 'fields', 'key', 'onDelete', 'history', 'ownerDefault', 'cache', 'meta']);
69
73
  function isZod(v) {
70
74
  return typeof v === 'object' && v !== null && '_zod' in v;
71
75
  }
72
- /** Пути правил refine/superRefine в схеме Zod — их нет в JSON Schema. */
76
+ /**
77
+ * Пути правил Zod, которых нет в JSON Schema: refine/superRefine (`refine`) и преобразования — `overwrite` (`.trim()`,
78
+ * `.toLowerCase()`, `.toUpperCase()`, `.normalize()` …) и `transform` (`.transform()`, `z.preprocess()`, `.pipe()`).
79
+ * Преобразования база и валидатор letopis не исполняют: их syncModels отклоняет (план 2.1, A11).
80
+ */
73
81
  function collectLost(schema, path, out) {
74
82
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
83
  const def = schema._zod.def;
76
- for (const c of def.checks ?? []) {
77
- if (c?._zod?.def?.check === 'custom')
78
- out.push({ path, kind: 'refine' });
79
- }
84
+ collectChecks(schema, path, out);
85
+ if (def.type === 'transform')
86
+ out.push({ path, kind: 'transform' });
80
87
  if (def.type === 'object')
81
88
  for (const [k, v] of Object.entries(def.shape))
82
89
  collectLost(v, `${path}.${k}`, out);
@@ -91,9 +98,24 @@ function collectLost(schema, path, out) {
91
98
  if (def.innerType)
92
99
  collectLost(def.innerType, path, out);
93
100
  }
101
+ /** Проверки самой схемы (без вложенных): refine/superRefine/check — `refine`, `overwrite` — преобразование. */
102
+ function collectChecks(schema, path, out) {
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ for (const c of schema._zod.def.checks ?? []) {
105
+ const check = c?._zod?.def?.check;
106
+ if (check === 'custom')
107
+ out.push({ path, kind: 'refine' });
108
+ else if (check === 'overwrite')
109
+ out.push({ path, kind: 'overwrite' });
110
+ }
111
+ }
94
112
  function makeModel(kind, name, defArg, optsArg) {
95
113
  if (!NAME.test(name))
96
- throw new LetopisError('invalid_class', `имя класса ${name} недопустимо`);
114
+ throw lerr('model.class_name_invalid', { name });
115
+ // имя метода цепочки или корня db: шаг с таким именем до хаба не дойдёт (план 2.0, этап 4, п. 2; 7.10 №3); связь с
116
+ // таким именем отклоняет база
117
+ if (kind === 'hub' && RESERVED_CLASS_NAMES.includes(name))
118
+ throw lerr('model.hub_name_reserved', { name });
97
119
  // короткая форма: hub(name, { поле: z…, … }, { key, meta, … })
98
120
  let def;
99
121
  // { fields: z.object(…) } без других ключей описания — полная форма, а не класс с единственным полем fields (план 2.0,
@@ -111,12 +133,12 @@ function makeModel(kind, name, defArg, optsArg) {
111
133
  const ownShape = !fields ? {} : fields instanceof z.ZodObject ? fields.shape : fields;
112
134
  for (const k of Object.keys(ownShape)) {
113
135
  if (k.startsWith('$'))
114
- throw new LetopisError('invalid_class', `${name}: имя поля ${k} начинается с $ — так загрузчик помечает служебные ключи`);
136
+ throw lerr('model.field_name_dollar', { cls: name, field: k });
115
137
  }
116
138
  const ownEnds = {};
117
139
  for (const [role, v] of Object.entries(def.ends ?? {})) {
118
140
  if (!NAME.test(role))
119
- throw new LetopisError('invalid_class', `${name}: имя конца ${role} недопустимо`);
141
+ throw lerr('model.end_name_invalid', { cls: name, role });
120
142
  let end = v instanceof End ? v : one(v);
121
143
  const rule = def.onDelete?.[role];
122
144
  if (rule)
@@ -136,28 +158,21 @@ function makeModel(kind, name, defArg, optsArg) {
136
158
  if (zd.type === 'default')
137
159
  zd = zd.innerType._zod.def;
138
160
  if (zd.type !== 'string')
139
- throw new LetopisError('invalid_class', `${name}: поле ключа ${k} должно быть обязательной строкой`);
161
+ throw lerr('model.key_field_type', { cls: name, field: k });
140
162
  }
141
163
  else if (ends[k]) {
142
164
  if (ends[k].many || !ends[k].required)
143
- throw new LetopisError('invalid_class', `${name}: конец ключа ${k} должен быть обязательным и одиночным`);
165
+ throw lerr('model.key_end_type', { cls: name, end: k });
144
166
  }
145
167
  else {
146
- throw new LetopisError('invalid_class', `${name}: ключ ссылается на неизвестное поле или конец ${k}`);
168
+ throw lerr('model.key_unknown_name', { cls: name, name: k });
147
169
  }
148
170
  }
149
171
  for (const [role, e] of Object.entries(ends)) {
150
172
  if (e.onDeleteRule === 'unset' && (e.required || (e.min ?? 0) > 0)) {
151
- throw new LetopisError('invalid_class', `${name}: onDelete unset недопустим для обязательного конца ${role}`);
173
+ throw lerr('model.unset_required_end', { cls: name, end: role });
152
174
  }
153
175
  }
154
- // агрегаты (план 1.1, §2.4): остальное — роль, поле суммы, цикл — сверяет база по описаниям источников
155
- for (const [field, a] of Object.entries(def.aggregate ?? {})) {
156
- if (!(field in shape))
157
- throw new LetopisError('invalid_class', `${name}: агрегатного поля ${field} нет среди полей класса`);
158
- if ((a.sum === undefined) === (a.count === undefined))
159
- throw new LetopisError('invalid_class', `${name}: агрегат ${field} — ровно одно из sum и count: true`);
160
- }
161
176
  // строгий верхний уровень — как в базе (resolved.schema: additionalProperties false)
162
177
  const schema = z.strictObject(shape);
163
178
  const model = {
@@ -217,20 +232,14 @@ function makeModel(kind, name, defArg, optsArg) {
217
232
  desc.cache = def.cache;
218
233
  if (def.meta)
219
234
  desc.meta = def.meta;
220
- if (def.derivedBy)
221
- desc.derivedBy = def.derivedBy;
222
- if (def.aggregate) {
223
- desc.aggregate = Object.fromEntries(Object.entries(def.aggregate).map(([field, a]) => [field, {
224
- ...(a.sum !== undefined ? { sum: a.sum } : { count: true }),
225
- of: typeof a.of === 'string' ? a.of : a.of.name,
226
- via: a.via,
227
- ...(a.where ? { where: a.where } : {}),
228
- }]));
229
- }
230
235
  return desc;
231
236
  },
232
237
  lostRules() {
233
238
  const out = [];
239
+ // проверки z.object(…).refine/superRefine/check — на уровне объекта (между полями): в описание класса идёт
240
+ // только shape, поэтому они тоже потеряны — путь — имя класса
241
+ if (fields instanceof z.ZodObject)
242
+ collectChecks(fields, name, out);
234
243
  for (const [k, v] of Object.entries(ownShape))
235
244
  collectLost(v, `${name}.${k}`, out);
236
245
  return out;
package/dist/ops.d.ts CHANGED
@@ -81,3 +81,38 @@ export declare const not: (v: Scalar | Op) => Op;
81
81
  * вместо всего фильтра шага, а не значением поля; условия внутри одного фильтра соединяются через «и».
82
82
  */
83
83
  export declare const or: (...filters: Record<string, unknown>[]) => Op;
84
+ /**
85
+ * Значение ключа в JSON-фильтре MCP: `value` — скаляр, `list` — массив скаляров, `range` — массив из двух скаляров,
86
+ * `text` — строка, `flag` — true или false, `true` — только true, `negation` — оператор или скаляр, `filters` — массив
87
+ * фильтров вместо всего фильтра шага (`$or`). По нему mcp.ts проверяет значение, а таблицы показывают форму.
88
+ */
89
+ export type OpArg = 'value' | 'list' | 'range' | 'text' | 'flag' | 'true' | 'negation' | 'filters';
90
+ /** Оператор фильтра в реестре. */
91
+ export interface OperatorSpec {
92
+ /** Имя фабрики в TypeScript: `import { gt } from 'letopis'`. */
93
+ readonly ts: string;
94
+ /** Сама фабрика. */
95
+ readonly make: (...args: never[]) => Op;
96
+ /** Ключ в JSON-фильтре MCP (`letopis_query`): `{ "price": { "$gt": 1000 } }`. */
97
+ readonly mcp: string;
98
+ /** Значение ключа MCP. */
99
+ readonly arg: OpArg;
100
+ /** Смысл для таблиц: README, шпаргалка, глава «Чтение» и справочник сайта. */
101
+ readonly ru: string;
102
+ }
103
+ /** Все 18 операторов в порядке таблиц. */
104
+ export declare const OPERATORS: readonly OperatorSpec[];
105
+ /** Вызов фабрики в таблицах: `between(a, b)`. */
106
+ export declare function opCall(o: OperatorSpec): string;
107
+ /** Ключ MCP со значением в таблицах: `{ "$between": [a, b] }`. */
108
+ export declare function opJson(o: OperatorSpec): string;
109
+ /** Ключ MCP с формой значения для описания letopis_query: `$between [a, b]`; у value, text и negation — только ключ. */
110
+ export declare function opHint(o: OperatorSpec): string;
111
+ /**
112
+ * Таблица операторов в Markdown — README, шпаргалка и справочник сайта: TypeScript, MCP, смысл; `mcp: false` — без
113
+ * колонки MCP (глава «Чтение» сайта: там цепочки на TypeScript). Вертикальная черта в ячейке экранируется, иначе она
114
+ * разрезала бы строку таблицы.
115
+ */
116
+ export declare function operatorTable({ mcp }?: {
117
+ mcp?: boolean;
118
+ }): string;
package/dist/ops.js CHANGED
@@ -82,3 +82,104 @@ export const not = (v) => op('not', v);
82
82
  * вместо всего фильтра шага, а не значением поля; условия внутри одного фильтра соединяются через «и».
83
83
  */
84
84
  export const or = (...filters) => op('or', filters);
85
+ /** Все 18 операторов в порядке таблиц. */
86
+ export const OPERATORS = [
87
+ {
88
+ ts: 'ne', make: ne, mcp: '$ne', arg: 'value',
89
+ ru: 'Не равно. Подходят и строки без поля или с `null`; `ne(null)` — строки, где у поля есть значение',
90
+ },
91
+ {
92
+ ts: 'gt', make: gt, mcp: '$gt', arg: 'value',
93
+ ru: 'Больше. Сравнение — по типу поля из описания класса (числа — как числа, даты — как даты), поле без описания — как текст; строки без поля не подходят',
94
+ },
95
+ {
96
+ ts: 'gte', make: gte, mcp: '$gte', arg: 'value',
97
+ ru: 'Больше или равно; сравнение — как у `gt`',
98
+ },
99
+ {
100
+ ts: 'lt', make: lt, mcp: '$lt', arg: 'value',
101
+ ru: 'Меньше; сравнение — как у `gt`',
102
+ },
103
+ {
104
+ ts: 'lte', make: lte, mcp: '$lte', arg: 'value',
105
+ ru: 'Меньше или равно; сравнение — как у `gt`',
106
+ },
107
+ {
108
+ ts: 'between', make: between, mcp: '$between', arg: 'range',
109
+ ru: 'От `a` до `b` включительно; сравнение — как у `gt`. Если `a` больше `b`, не подходит ничего',
110
+ },
111
+ {
112
+ ts: 'inList', make: inList, mcp: '$in', arg: 'list',
113
+ ru: 'Одно из значений списка. Пустой список не находит ничего; `null` в списке не находит строк без значения — для них `isNull`',
114
+ },
115
+ {
116
+ ts: 'like', make: like, mcp: '$like', arg: 'text',
117
+ ru: 'Текст по шаблону с учётом регистра: `%` — любые символы, в том числе ни одного, `_` — ровно один; буквальные `%` и `_` — с обратной косой чертой',
118
+ },
119
+ {
120
+ ts: 'ilike', make: ilike, mcp: '$ilike', arg: 'text',
121
+ ru: 'Как `like`, но без учёта регистра',
122
+ },
123
+ {
124
+ ts: 'starts', make: starts, mcp: '$starts', arg: 'text',
125
+ ru: 'Текст начинается с `s`, с учётом регистра. Это `like(s + \'%\')`: `%` и `_` внутри `s` не экранируются и работают как в шаблоне',
126
+ },
127
+ {
128
+ ts: 'ends', make: ends, mcp: '$ends', arg: 'text',
129
+ ru: 'Текст заканчивается на `s`, с учётом регистра. Это `like(\'%\' + s)`: `%` и `_` внутри `s` не экранируются и работают как в шаблоне',
130
+ },
131
+ {
132
+ ts: 'has', make: has, mcp: '$has', arg: 'value',
133
+ ru: 'Поле-массив содержит элемент `v`. Тип важен: `has(5)` найдёт число 5, но не строку `\'5\'`',
134
+ },
135
+ {
136
+ ts: 'hasAny', make: hasAny, mcp: '$hasAny', arg: 'list',
137
+ ru: 'Поле-массив содержит хотя бы один элемент списка. Находит только строковые элементы; пустой список не находит ничего',
138
+ },
139
+ {
140
+ ts: 'hasAll', make: hasAll, mcp: '$hasAll', arg: 'list',
141
+ ru: 'Поле-массив содержит все элементы списка, в любом порядке. Тип важен, как у `has`; пустой список подходит любой строке, даже без поля',
142
+ },
143
+ {
144
+ ts: 'exists', make: exists, mcp: '$exists', arg: 'flag',
145
+ ru: 'Поле есть в данных (`true`, по умолчанию) или его нет (`false`); значение не важно — поле с `null` тоже есть',
146
+ },
147
+ {
148
+ ts: 'isNull', make: isNull, mcp: '$isNull', arg: 'true',
149
+ ru: 'В поле `null` или поля нет совсем; отличить одно от другого помогает `exists`',
150
+ },
151
+ {
152
+ ts: 'not', make: not, mcp: '$not', arg: 'negation',
153
+ ru: 'Отрицание: `x` — оператор (`not(gte(1000))` — меньше 1000 или поля нет) или простое значение — тогда это «не равно», как `ne`',
154
+ },
155
+ {
156
+ ts: 'or', make: or, mcp: '$or', arg: 'filters',
157
+ ru: 'Хотя бы один из фильтров. Ставится вместо всего фильтра шага, а не значением поля; условия внутри одного фильтра соединяются через «и»',
158
+ },
159
+ ];
160
+ /** Вызов фабрики в таблицах: `between(a, b)`. */
161
+ export function opCall(o) {
162
+ const args = { value: 'v', list: '[v, …]', range: 'a, b', text: 's', flag: 'b = true', true: '', negation: 'x', filters: 'f1, f2, …' };
163
+ return `${o.ts}(${args[o.arg]})`;
164
+ }
165
+ /** Ключ MCP со значением в таблицах: `{ "$between": [a, b] }`. */
166
+ export function opJson(o) {
167
+ const vals = { value: 'v', list: '[v, …]', range: '[a, b]', text: '"s"', flag: 'b', true: 'true', negation: 'x', filters: '[f1, f2, …]' };
168
+ return `{ "${o.mcp}": ${vals[o.arg]} }`;
169
+ }
170
+ /** Ключ MCP с формой значения для описания letopis_query: `$between [a, b]`; у value, text и negation — только ключ. */
171
+ export function opHint(o) {
172
+ const vals = { list: '[..]', range: '[a, b]', flag: 'true|false', true: 'true', filters: '[filter, …]' };
173
+ return vals[o.arg] ? `${o.mcp} ${vals[o.arg]}` : o.mcp;
174
+ }
175
+ /**
176
+ * Таблица операторов в Markdown — README, шпаргалка и справочник сайта: TypeScript, MCP, смысл; `mcp: false` — без
177
+ * колонки MCP (глава «Чтение» сайта: там цепочки на TypeScript). Вертикальная черта в ячейке экранируется, иначе она
178
+ * разрезала бы строку таблицы.
179
+ */
180
+ export function operatorTable({ mcp = true } = {}) {
181
+ const cell = (s) => s.replaceAll('|', '\\|');
182
+ const head = mcp ? '| TypeScript | MCP | Смысл |' : '| TypeScript | Смысл |';
183
+ const rows = OPERATORS.map((o) => `| ${[`\`${opCall(o)}\``, ...(mcp ? [`\`${opJson(o)}\``] : []), o.ru].map(cell).join(' | ')} |`);
184
+ return [head, mcp ? '|---|---|---|' : '|---|---|', ...rows].join('\n');
185
+ }
package/dist/pglite.d.ts CHANGED
@@ -1,12 +1,3 @@
1
- /**
2
- * База для разработки без установленного PostgreSQL (решение 6 точки А): PGlite (PostgreSQL 18 в
3
- * WebAssembly) с pgcrypto, доступный postgres.js через pglite-socket. Одно соединение: годится для
4
- * разработки и быстрых тестов логики, но не для гонок и не вместо postgres:18 в CI; LISTEN из
5
- * второго соединения нет — connect({ listen: false, max: 1 }).
6
- *
7
- * Пакеты @electric-sql/pglite и @electric-sql/pglite-socket — необязательные peer-зависимости:
8
- * основная точка входа их не импортирует, up({ pglite }) загружает их по требованию.
9
- */
10
1
  export interface PgliteOptions {
11
2
  /** Каталог данных (по умолчанию — в памяти, данные теряются при остановке). */
12
3
  dataDir?: string;
package/dist/pglite.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * Пакеты @electric-sql/pglite и @electric-sql/pglite-socket — необязательные peer-зависимости:
8
8
  * основная точка входа их не импортирует, up({ pglite }) загружает их по требованию.
9
9
  */
10
+ import { lmsg } from './errors.js';
10
11
  /** Запустить PGlite с pgcrypto и сокетом для postgres.js. */
11
12
  export async function startPglite(opts = {}) {
12
13
  const load = async (name) => {
@@ -14,7 +15,8 @@ export async function startPglite(opts = {}) {
14
15
  return await import(name);
15
16
  }
16
17
  catch {
17
- throw new Error(`letopis.up: для pglite нужен пакет ${name} — npm i -D @electric-sql/pglite @electric-sql/pglite-socket`);
18
+ // обычный Error, как у up(): подходящего кода нет
19
+ throw new Error(`letopis.up: ${lmsg('boot.pglite_package', { name })}`);
18
20
  }
19
21
  };
20
22
  const { PGlite } = await load('@electric-sql/pglite');
@@ -9,18 +9,7 @@ export interface EndInfo {
9
9
  onDelete?: 'restrict' | 'cascade' | 'unset';
10
10
  meta?: Record<string, unknown>;
11
11
  }
12
- /**
13
- * Агрегат в базе (план 1.1, §2.4): поле — сумма поля sum или число (count) живых строк класса of с потомками того же
14
- * арендатора, чей одиночный конец via ведёт на строку; where — JSON Schema над данными строки-источника.
15
- */
16
- export interface AggregateInfo {
17
- sum?: string;
18
- count?: true;
19
- of: string;
20
- via: string;
21
- where?: JsonSchema;
22
- }
23
- /** Тип листа данных для фильтров, сортировки и агрегатов (замена fieldTypeOf 0.21). */
12
+ /** Тип листа данных для фильтров, сортировки и агрегатов. */
24
13
  export type LeafType = 'text' | 'numeric' | 'boolean' | 'timestamptz' | 'date' | 'uuid' | 'jsonb';
25
14
  export interface ClassInfo {
26
15
  name: string;
@@ -34,16 +23,13 @@ export interface ClassInfo {
34
23
  schema: JsonSchema;
35
24
  ends: Record<string, EndInfo>;
36
25
  key: string[];
37
- history: unknown;
26
+ /** Ведёт ли класс журнал версий (`history: false` — нет). */
27
+ history: boolean;
38
28
  ownerDefault: 'tenant' | 'actor';
39
29
  cache?: {
40
30
  ttl?: number;
41
31
  };
42
32
  meta: Record<string, unknown>;
43
- /** Производный класс: строки пишет только проектор с этим именем (план 1.1, §2.4). */
44
- derivedBy?: string;
45
- /** Агрегатные поля: их считает база, приложение не пишет (план 1.1, §2.4). */
46
- aggregate?: Record<string, AggregateInfo>;
47
33
  /** Валидатор на TypeScript (компилируется при первом обращении). */
48
34
  validator(): Validator;
49
35
  }
@@ -55,8 +41,6 @@ interface ClassRow {
55
41
  resolved: Record<string, unknown>;
56
42
  };
57
43
  }
58
- /** Агрегатные поля класса: их считает база (план 1.1, §2.4). */
59
- export declare const aggregateFields: (cls: Pick<ClassInfo, "aggregate">) => string[];
60
44
  export declare class Registry {
61
45
  private byName;
62
46
  private byAlias;