letopis 1.4.1 → 2.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 (113) hide show
  1. package/AGENT-CHEATSHEET.md +329 -278
  2. package/CHANGELOG.md +558 -1
  3. package/README.md +606 -1037
  4. package/dist/acl.d.ts +78 -1
  5. package/dist/acl.js +264 -13
  6. package/dist/admin.d.ts +10 -141
  7. package/dist/admin.js +24 -175
  8. package/dist/auth.d.ts +2 -2
  9. package/dist/auth.js +5 -15
  10. package/dist/cache.d.ts +8 -1
  11. package/dist/cache.js +33 -10
  12. package/dist/chain.d.ts +19 -41
  13. package/dist/chain.js +169 -201
  14. package/dist/cli.js +51 -88
  15. package/dist/demo/booking.d.ts +352 -36
  16. package/dist/describe.d.ts +47 -0
  17. package/dist/describe.js +195 -0
  18. package/dist/errors.d.ts +59 -13
  19. package/dist/errors.js +190 -29
  20. package/dist/index.d.ts +52 -71
  21. package/dist/index.js +245 -130
  22. package/dist/internal.d.ts +12 -0
  23. package/dist/internal.js +5 -0
  24. package/dist/jsonschema.js +18 -17
  25. package/dist/load.d.ts +20 -12
  26. package/dist/load.js +225 -186
  27. package/dist/mcp.d.ts +5 -7
  28. package/dist/mcp.js +171 -215
  29. package/dist/messages/access.d.ts +90 -0
  30. package/dist/messages/access.js +113 -0
  31. package/dist/messages/api.d.ts +171 -0
  32. package/dist/messages/api.js +215 -0
  33. package/dist/messages/boot.d.ts +201 -0
  34. package/dist/messages/boot.js +256 -0
  35. package/dist/messages/codes.d.ts +172 -0
  36. package/dist/messages/codes.js +173 -0
  37. package/dist/messages/index.d.ts +95 -0
  38. package/dist/messages/index.js +118 -0
  39. package/dist/messages/mcp.d.ts +306 -0
  40. package/dist/messages/mcp.js +387 -0
  41. package/dist/messages/model.d.ts +233 -0
  42. package/dist/messages/model.js +283 -0
  43. package/dist/messages/query.d.ts +131 -0
  44. package/dist/messages/query.js +158 -0
  45. package/dist/messages/sql-access.d.ts +70 -0
  46. package/dist/messages/sql-access.js +87 -0
  47. package/dist/messages/sql-core.d.ts +150 -0
  48. package/dist/messages/sql-core.js +181 -0
  49. package/dist/messages/sql-time.d.ts +71 -0
  50. package/dist/messages/sql-time.js +88 -0
  51. package/dist/messages/sql-write.d.ts +246 -0
  52. package/dist/messages/sql-write.js +270 -0
  53. package/dist/messages/time.d.ts +23 -0
  54. package/dist/messages/time.js +29 -0
  55. package/dist/messages/types.d.ts +27 -0
  56. package/dist/messages/types.js +5 -0
  57. package/dist/messages/write.d.ts +405 -0
  58. package/dist/messages/write.js +509 -0
  59. package/dist/model.d.ts +73 -60
  60. package/dist/model.js +26 -29
  61. package/dist/ops.d.ts +35 -0
  62. package/dist/ops.js +101 -0
  63. package/dist/pglite.d.ts +0 -9
  64. package/dist/pglite.js +3 -1
  65. package/dist/registry.d.ts +3 -19
  66. package/dist/registry.js +3 -16
  67. package/dist/signals.d.ts +5 -9
  68. package/dist/signals.js +5 -9
  69. package/dist/sql.d.ts +13 -6
  70. package/dist/sql.js +176 -83
  71. package/dist/sync.d.ts +9 -3
  72. package/dist/sync.js +51 -21
  73. package/dist/tx.d.ts +26 -1
  74. package/dist/tx.js +44 -14
  75. package/dist/typed.d.ts +240 -45
  76. package/dist/types.d.ts +23 -91
  77. package/dist/types.js +12 -14
  78. package/dist/up.d.ts +53 -32
  79. package/dist/up.js +184 -73
  80. package/dist/uuid.js +5 -5
  81. package/dist/validate.d.ts +13 -1
  82. package/dist/validate.js +61 -29
  83. package/dist/watch.d.ts +11 -10
  84. package/dist/watch.js +33 -70
  85. package/dist/write.d.ts +9 -16
  86. package/dist/write.js +118 -187
  87. package/llms.txt +22 -25
  88. package/package.json +8 -11
  89. package/sql/10-core.sql +7 -50
  90. package/sql/15-errors.sql +64 -10
  91. package/sql/20-context.sql +7 -6
  92. package/sql/30-validate.sql +35 -27
  93. package/sql/40-class.sql +20 -114
  94. package/sql/50-acl.sql +8 -13
  95. package/sql/60-write.sql +220 -667
  96. package/sql/70-read.sql +20 -16
  97. package/sql/80-auth.sql +24 -27
  98. package/sql/90-time.sql +70 -859
  99. package/sql/95-seed.system.sql +7 -14
  100. package/sql/99-revision.sql +2 -1
  101. package/sql/README.md +32 -24
  102. package/sql/migrate/README.md +14 -0
  103. package/AGENT-CHEATSHEET.en.md +0 -485
  104. package/MIGRATION.md +0 -190
  105. package/README.en.md +0 -2859
  106. package/dist/import.d.ts +0 -185
  107. package/dist/import.js +0 -820
  108. package/dist/project.d.ts +0 -350
  109. package/dist/project.js +0 -1224
  110. package/sql/75-history.sql +0 -358
  111. package/sql/88-derived.sql +0 -1235
  112. package/sql/migrate/0006-changed-paths.sql +0 -3
  113. 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,21 @@ 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']>>;
200
213
  /** Связь — отношение между объектами со своими данными (бронь, навык). */
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>>;
214
+ 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>;
215
+ 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']>>;
203
216
  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,12 +59,17 @@ 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}$/;
71
+ /** Ключи полной формы описания класса (ClassDef). */
72
+ const DEF_KEYS = new Set(['alias', 'extends', 'abstract', 'ends', 'fields', 'key', 'onDelete', 'history', 'ownerDefault', 'cache', 'meta']);
67
73
  function isZod(v) {
68
74
  return typeof v === 'object' && v !== null && '_zod' in v;
69
75
  }
@@ -91,10 +97,18 @@ function collectLost(schema, path, out) {
91
97
  }
92
98
  function makeModel(kind, name, defArg, optsArg) {
93
99
  if (!NAME.test(name))
94
- throw new LetopisError('invalid_class', `имя класса ${name} недопустимо`);
100
+ throw lerr('model.class_name_invalid', { name });
101
+ // имя метода цепочки или корня db: шаг с таким именем до хаба не дойдёт (план 2.0, этап 4, п. 2; 7.10 №3); связь с
102
+ // таким именем отклоняет база
103
+ if (kind === 'hub' && RESERVED_CLASS_NAMES.includes(name))
104
+ throw lerr('model.hub_name_reserved', { name });
95
105
  // короткая форма: hub(name, { поле: z…, … }, { key, meta, … })
96
106
  let def;
97
- const isShape = defArg && typeof defArg === 'object' && Object.values(defArg).length > 0 && Object.values(defArg).every(isZod);
107
+ // { fields: z.object(…) } без других ключей описания — полная форма, а не класс с единственным полем fields (план 2.0,
108
+ // 7.5 №2); короткая форма — все значения Zod и хотя бы один ключ вне описания класса
109
+ const fullForm = !!defArg && typeof defArg === 'object' && defArg.fields instanceof z.ZodObject
110
+ && Object.keys(defArg).every((k) => DEF_KEYS.has(k));
111
+ const isShape = !fullForm && defArg && typeof defArg === 'object' && Object.values(defArg).length > 0 && Object.values(defArg).every(isZod);
98
112
  if (optsArg !== undefined || isShape) {
99
113
  def = { ...optsArg, fields: (defArg ?? {}) };
100
114
  }
@@ -105,12 +119,12 @@ function makeModel(kind, name, defArg, optsArg) {
105
119
  const ownShape = !fields ? {} : fields instanceof z.ZodObject ? fields.shape : fields;
106
120
  for (const k of Object.keys(ownShape)) {
107
121
  if (k.startsWith('$'))
108
- throw new LetopisError('invalid_class', `${name}: имя поля ${k} начинается с $ — так загрузчик помечает служебные ключи`);
122
+ throw lerr('model.field_name_dollar', { cls: name, field: k });
109
123
  }
110
124
  const ownEnds = {};
111
125
  for (const [role, v] of Object.entries(def.ends ?? {})) {
112
126
  if (!NAME.test(role))
113
- throw new LetopisError('invalid_class', `${name}: имя конца ${role} недопустимо`);
127
+ throw lerr('model.end_name_invalid', { cls: name, role });
114
128
  let end = v instanceof End ? v : one(v);
115
129
  const rule = def.onDelete?.[role];
116
130
  if (rule)
@@ -130,28 +144,21 @@ function makeModel(kind, name, defArg, optsArg) {
130
144
  if (zd.type === 'default')
131
145
  zd = zd.innerType._zod.def;
132
146
  if (zd.type !== 'string')
133
- throw new LetopisError('invalid_class', `${name}: поле ключа ${k} должно быть обязательной строкой`);
147
+ throw lerr('model.key_field_type', { cls: name, field: k });
134
148
  }
135
149
  else if (ends[k]) {
136
150
  if (ends[k].many || !ends[k].required)
137
- throw new LetopisError('invalid_class', `${name}: конец ключа ${k} должен быть обязательным и одиночным`);
151
+ throw lerr('model.key_end_type', { cls: name, end: k });
138
152
  }
139
153
  else {
140
- throw new LetopisError('invalid_class', `${name}: ключ ссылается на неизвестное поле или конец ${k}`);
154
+ throw lerr('model.key_unknown_name', { cls: name, name: k });
141
155
  }
142
156
  }
143
157
  for (const [role, e] of Object.entries(ends)) {
144
158
  if (e.onDeleteRule === 'unset' && (e.required || (e.min ?? 0) > 0)) {
145
- throw new LetopisError('invalid_class', `${name}: onDelete unset недопустим для обязательного конца ${role}`);
159
+ throw lerr('model.unset_required_end', { cls: name, end: role });
146
160
  }
147
161
  }
148
- // агрегаты (план 1.1, §2.4): остальное — роль, поле суммы, цикл — сверяет база по описаниям источников
149
- for (const [field, a] of Object.entries(def.aggregate ?? {})) {
150
- if (!(field in shape))
151
- throw new LetopisError('invalid_class', `${name}: агрегатного поля ${field} нет среди полей класса`);
152
- if ((a.sum === undefined) === (a.count === undefined))
153
- throw new LetopisError('invalid_class', `${name}: агрегат ${field} — ровно одно из sum и count: true`);
154
- }
155
162
  // строгий верхний уровень — как в базе (resolved.schema: additionalProperties false)
156
163
  const schema = z.strictObject(shape);
157
164
  const model = {
@@ -211,16 +218,6 @@ function makeModel(kind, name, defArg, optsArg) {
211
218
  desc.cache = def.cache;
212
219
  if (def.meta)
213
220
  desc.meta = def.meta;
214
- if (def.derivedBy)
215
- desc.derivedBy = def.derivedBy;
216
- if (def.aggregate) {
217
- desc.aggregate = Object.fromEntries(Object.entries(def.aggregate).map(([field, a]) => [field, {
218
- ...(a.sum !== undefined ? { sum: a.sum } : { count: true }),
219
- of: typeof a.of === 'string' ? a.of : a.of.name,
220
- via: a.via,
221
- ...(a.where ? { where: a.where } : {}),
222
- }]));
223
- }
224
221
  return desc;
225
222
  },
226
223
  lostRules() {
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;
package/dist/registry.js CHANGED
@@ -3,19 +3,8 @@
3
3
  * (собственные классы арендатора и системные), поиск по имени и алиасу, предки и потомки,
4
4
  * типы листьев для фильтров и валидатор на TypeScript, скомпилированный из того же описания.
5
5
  */
6
- import { LetopisError } from './errors.js';
6
+ import { lerr } from './errors.js';
7
7
  import { compileValidator } from './validate.js';
8
- /** Агрегатные поля класса: их считает база (план 1.1, §2.4). */
9
- export const aggregateFields = (cls) => Object.keys(cls.aggregate ?? {});
10
- /** Описание агрегатов из resolved — без скомпилированного условия (match): оно для базы. */
11
- function aggregateOf(agg) {
12
- return Object.fromEntries(Object.entries(agg).map(([field, d]) => [field, {
13
- ...(typeof d.sum === 'string' ? { sum: d.sum } : { count: true }),
14
- of: d.of,
15
- via: d.via,
16
- ...(d.where && typeof d.where === 'object' ? { where: d.where } : {}),
17
- }]));
18
- }
19
8
  export class Registry {
20
9
  byName = new Map();
21
10
  byAlias = new Map();
@@ -37,12 +26,10 @@ export class Registry {
37
26
  schema: res.schema,
38
27
  ends: (res.ends ?? {}),
39
28
  key: (res.key ?? []),
40
- history: res.history,
29
+ history: res.history !== false,
41
30
  ownerDefault: (res.ownerDefault ?? 'tenant'),
42
31
  cache: (res.cache ?? undefined),
43
32
  meta: (res.meta ?? {}),
44
- ...(typeof res.derivedBy === 'string' ? { derivedBy: res.derivedBy } : {}),
45
- ...(res.aggregate && typeof res.aggregate === 'object' ? { aggregate: aggregateOf(res.aggregate) } : {}),
46
33
  validator: () => (compiled ??= compileValidator(res.schema)),
47
34
  };
48
35
  this.byName.set(info.name, info);
@@ -59,7 +46,7 @@ export class Registry {
59
46
  resolve(name) {
60
47
  const c = this.find(name);
61
48
  if (!c)
62
- throw new LetopisError('invalid_class', `класс ${name} не найден`);
49
+ throw lerr('model.class_not_found', { name });
63
50
  return c;
64
51
  }
65
52
  all() {