@bjornpagen/bumbledb 0.2.0 → 0.3.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 (84) hide show
  1. package/COOKBOOK.md +462 -406
  2. package/README.md +66 -31
  3. package/dist/closed.d.ts +121 -25
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +108 -42
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +12 -1
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +20 -5
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +100 -55
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js +36 -10
  14. package/dist/face.js.map +1 -1
  15. package/dist/fields.d.ts +50 -79
  16. package/dist/fields.d.ts.map +1 -1
  17. package/dist/fields.js +20 -53
  18. package/dist/fields.js.map +1 -1
  19. package/dist/index.d.ts +17 -12
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -6
  22. package/dist/index.js.map +1 -1
  23. package/dist/law.d.ts +224 -0
  24. package/dist/law.d.ts.map +1 -0
  25. package/dist/law.js +224 -0
  26. package/dist/law.js.map +1 -0
  27. package/dist/lower.d.ts +17 -10
  28. package/dist/lower.d.ts.map +1 -1
  29. package/dist/lower.js +34 -23
  30. package/dist/lower.js.map +1 -1
  31. package/dist/native.d.ts +6 -2
  32. package/dist/native.d.ts.map +1 -1
  33. package/dist/native.js.map +1 -1
  34. package/dist/query/atom.d.ts +88 -50
  35. package/dist/query/atom.d.ts.map +1 -1
  36. package/dist/query/atom.js +4 -1
  37. package/dist/query/atom.js.map +1 -1
  38. package/dist/query/lower.d.ts +71 -56
  39. package/dist/query/lower.d.ts.map +1 -1
  40. package/dist/query/lower.js +90 -43
  41. package/dist/query/lower.js.map +1 -1
  42. package/dist/query/predicate.d.ts +10 -9
  43. package/dist/query/predicate.d.ts.map +1 -1
  44. package/dist/query/predicate.js +2 -2
  45. package/dist/query/predicate.js.map +1 -1
  46. package/dist/query/scope.d.ts +76 -41
  47. package/dist/query/scope.d.ts.map +1 -1
  48. package/dist/query/scope.js +77 -30
  49. package/dist/query/scope.js.map +1 -1
  50. package/dist/query/select.d.ts +5 -5
  51. package/dist/query/select.d.ts.map +1 -1
  52. package/dist/relation.d.ts +21 -8
  53. package/dist/relation.d.ts.map +1 -1
  54. package/dist/relation.js +13 -7
  55. package/dist/relation.js.map +1 -1
  56. package/dist/schema.d.ts +41 -3
  57. package/dist/schema.d.ts.map +1 -1
  58. package/dist/schema.js +16 -2
  59. package/dist/schema.js.map +1 -1
  60. package/dist/spec.d.ts +7 -6
  61. package/dist/spec.d.ts.map +1 -1
  62. package/dist/spec.js.map +1 -1
  63. package/dist/statements.d.ts +61 -31
  64. package/dist/statements.d.ts.map +1 -1
  65. package/dist/statements.js +22 -17
  66. package/dist/statements.js.map +1 -1
  67. package/package.json +2 -2
  68. package/src/closed.ts +243 -68
  69. package/src/db.ts +23 -5
  70. package/src/face.ts +162 -84
  71. package/src/fields.ts +57 -136
  72. package/src/index.ts +42 -16
  73. package/src/law.ts +519 -0
  74. package/src/lower.ts +36 -23
  75. package/src/native.ts +6 -2
  76. package/src/query/atom.ts +105 -58
  77. package/src/query/lower.ts +271 -139
  78. package/src/query/predicate.ts +43 -33
  79. package/src/query/scope.ts +125 -49
  80. package/src/query/select.ts +5 -5
  81. package/src/relation.ts +15 -9
  82. package/src/schema.ts +48 -7
  83. package/src/spec.ts +7 -6
  84. package/src/statements.ts +83 -43
package/src/law.ts ADDED
@@ -0,0 +1,519 @@
1
+ /**
2
+ * The law-typing engine (owner ruling 2026-07-18, "option 2, zero debate"):
3
+ * THE LAWS TYPE THE COLUMNS. Domains are declared nowhere — `schema()`
4
+ * computes every field's domain FROM the statement list, at BOTH the type
5
+ * level (this module's type machinery, reading the statements tuple type)
6
+ * and at runtime (a plain union-find over the same pairs), and the two
7
+ * tiers are the same computation by construction.
8
+ *
9
+ * The three class laws (ratified; implemented exactly):
10
+ *
11
+ * 1. GENERATORS — a `fresh` field is a generator and names its class by
12
+ * its declaration coordinate (`"Account.id"`); a closed relation's
13
+ * synthetic id is a generator named `"Kind.id"`.
14
+ * 2. GENERATOR-LESS classes are named by their least member coordinate
15
+ * in relation-declaration × field-declaration order (readable off the
16
+ * relation record and each member's frozen field list at the VALUE
17
+ * tier — deterministic, pinned forever; the wire reads only this
18
+ * tier). At the TYPE tier the same class is carried as its
19
+ * member-coordinate SET (see {@link ClassOfCoord}): TypeScript's
20
+ * union member order is not observably deterministic, so a type-level
21
+ * least-member pick would drift between compilations — the set is the
22
+ * canonical deterministic spelling, the runtime name is always a
23
+ * member of it, and the join judgment is identical at both tiers.
24
+ * 3. BARE — a field in no law has NO class and pairs only with bare in
25
+ * queries (the deliberate sum-domain pointer stays legal).
26
+ *
27
+ * THE WALL: a class containing more than one generator is a contradiction
28
+ * (two mints cannot share a carrier) — a schema-level COMPILE error (the
29
+ * named, self-locating {@link ClassWall}: which generator coordinates
30
+ * collided, through which paired slots) with a construction-time runtime
31
+ * twin (`computeClasses` throws with the same content, naming the exact
32
+ * statement).
33
+ *
34
+ * Every paired face of the statement tuple unions its positionwise field
35
+ * slots: containment (ψ-selected targets included — a selection changes
36
+ * pairing not at all), the `==` bijection, and window source/target pairs.
37
+ * `key()` statements pair nothing (an FD constrains one relation's own
38
+ * rows; it identifies no carriers).
39
+ *
40
+ * The type tier reads pairs off the statement types' exact face data, so
41
+ * spell the statement list INLINE in `schema()` (the `const` type
42
+ * parameter keeps the tuple precise). A widened `Statement[]` list
43
+ * degrades the TYPE tier to generators-only (no pair is readable off a
44
+ * widened type) — the runtime map stays complete and authoritative, and
45
+ * the wire lowering reads only the runtime map. Every loop below is
46
+ * tail-recursive with an accumulator, so the machinery rides TypeScript's
47
+ * tail-recursion elimination at primer scale (~40 relations, ~200 slots,
48
+ * ~123 statements); should a schema ever exceed the compiler's limits, tsc
49
+ * fails LOUDLY with its own instantiation-depth error — the map is never
50
+ * silently widened.
51
+ */
52
+
53
+ import * as errors from "@superbuilders/errors"
54
+ import type { AnyClosed } from "#closed.ts"
55
+ import type { FaceData } from "#face.ts"
56
+ import type { AnyRelation, RelationFields } from "#relation.ts"
57
+ import type { SchemaRelation, SchemaRelations } from "#schema.ts"
58
+ import { renderStatement, type Statement } from "#statements.ts"
59
+
60
+ // ————————————————————————————————————————————————————————————————————————
61
+ // The class-map shapes.
62
+ // ————————————————————————————————————————————————————————————————————————
63
+
64
+ /** One relation's computed classes: field name → class name, `undefined` = bare. */
65
+ type RelationClasses = { readonly [field: string]: string | undefined }
66
+
67
+ /**
68
+ * The class map a schema carries — relation name → field name → the
69
+ * computed class name (`undefined` = bare). THE domain authority: queries
70
+ * and the wire lowering read domains from here and nowhere else. The wide
71
+ * shape is the default every `Schema`-generic surface accepts; a concrete
72
+ * schema's `classes` property carries the EXACT computed map
73
+ * ({@link ClassesOf}) at the type level and the frozen runtime twin
74
+ * (`computeClasses`) at the value level — one property, two tiers, one
75
+ * computation.
76
+ */
77
+ type SchemaClasses = { readonly [relation: string]: RelationClasses }
78
+
79
+ /** Looks one relation's class record up in a schema's class map (absent relation = no classes). */
80
+ type ClassRecordOf<Classes extends SchemaClasses, N extends string> = N extends keyof Classes
81
+ ? Classes[N]
82
+ : Record<never, never>
83
+
84
+ /** Looks one field's class up in a relation's class record (absent field = bare). */
85
+ type ClassLookup<CR, K> = K extends keyof CR ? CR[K] & (string | undefined) : undefined
86
+
87
+ // ————————————————————————————————————————————————————————————————————————
88
+ // Coordinates.
89
+ // ————————————————————————————————————————————————————————————————————————
90
+
91
+ /** A closed member's declared payload-column record (`never` on ordinary relations). */
92
+ type MemberColumns<M> = M extends AnyClosed ? M["columns"] : never
93
+
94
+ /** The field names of one schema member: a relation's declared fields; a closed relation's sealed `id` + columns. */
95
+ type MemberFieldNames<M extends SchemaRelation> = M extends AnyClosed
96
+ ? "id" | (keyof MemberColumns<M> & string)
97
+ : M extends AnyRelation
98
+ ? keyof RelationFields<M> & string
99
+ : never
100
+
101
+ /** The fresh-marked field names of one field block. */
102
+ type FreshFieldNames<Fields> = {
103
+ [F in keyof Fields & string]: Fields[F] extends { readonly fresh: true } ? F : never
104
+ }[keyof Fields & string]
105
+
106
+ /** One member's generator coordinates: fresh fields; a closed relation's synthetic id. */
107
+ type MemberGenerators<N extends string, M extends SchemaRelation> = M extends AnyClosed
108
+ ? `${N}.id`
109
+ : M extends AnyRelation
110
+ ? `${N}.${FreshFieldNames<RelationFields<M>>}`
111
+ : never
112
+
113
+ /** Every generator coordinate of a relation record (a union). */
114
+ type GeneratorsOf<Rels extends SchemaRelations> = {
115
+ [N in keyof Rels & string]: MemberGenerators<N, Rels[N]>
116
+ }[keyof Rels & string]
117
+
118
+ // ————————————————————————————————————————————————————————————————————————
119
+ // Pairs: the positionwise slot pairs of every paired face.
120
+ // ————————————————————————————————————————————————————————————————————————
121
+
122
+ /** One paired-slot pair of coordinates. */
123
+ type Pair = readonly [string, string]
124
+
125
+ /** A list of slot pairs. */
126
+ type PairList = readonly Pair[]
127
+
128
+ /** Zips two faces' projections into coordinate pairs, positionwise. */
129
+ type ZipCoords<
130
+ SN extends string,
131
+ SP extends readonly string[],
132
+ TN extends string,
133
+ TP extends readonly string[],
134
+ Acc extends PairList = []
135
+ > = SP extends readonly [infer SH extends string, ...infer ST extends readonly string[]]
136
+ ? TP extends readonly [infer TH extends string, ...infer TT extends readonly string[]]
137
+ ? ZipCoords<SN, ST, TN, TT, readonly [...Acc, readonly [`${SN}.${SH}`, `${TN}.${TH}`]]>
138
+ : Acc
139
+ : Acc
140
+
141
+ /**
142
+ * One statement's slot pairs: containments (bidirectional included — pair
143
+ * unions are symmetric) and windows pair their two faces positionwise;
144
+ * `key()` pairs nothing. A widened face (owner name or projection no
145
+ * longer literal) contributes nothing — the runtime map stays complete.
146
+ */
147
+ type StatementPairs<St extends Statement> = St["data"] extends {
148
+ readonly source: infer S extends FaceData
149
+ readonly target: infer T extends FaceData
150
+ }
151
+ ? string extends S["owner"]["name"]
152
+ ? []
153
+ : string extends T["owner"]["name"]
154
+ ? []
155
+ : ZipCoords<S["owner"]["name"], S["projection"], T["owner"]["name"], T["projection"]>
156
+ : []
157
+
158
+ /** Every slot pair of a statements tuple, in written order. */
159
+ type PairsOf<Stmts extends readonly Statement[], Acc extends PairList = []> = Stmts extends readonly [
160
+ infer H extends Statement,
161
+ ...infer T extends readonly Statement[]
162
+ ]
163
+ ? PairsOf<T, readonly [...Acc, ...StatementPairs<H>]>
164
+ : Acc
165
+
166
+ // ————————————————————————————————————————————————————————————————————————
167
+ // Union-find over the pairs: connected components as coordinate unions.
168
+ // ————————————————————————————————————————————————————————————————————————
169
+
170
+ /** The component (a union of coordinates) containing `X`, or `never` when `X` is in none. */
171
+ type CompOf<Comps extends readonly string[], X extends string> = Comps extends readonly [
172
+ infer H extends string,
173
+ ...infer T extends readonly string[]
174
+ ]
175
+ ? [X] extends [H]
176
+ ? H
177
+ : CompOf<T, X>
178
+ : never
179
+
180
+ /** Rebuilds the component list without the component `C` (components are disjoint; identity is mutual extension). */
181
+ type WithoutComp<
182
+ Comps extends readonly string[],
183
+ C extends string,
184
+ Acc extends readonly string[] = []
185
+ > = Comps extends readonly [infer H extends string, ...infer T extends readonly string[]]
186
+ ? [H, C] extends [C, H]
187
+ ? WithoutComp<T, C, Acc>
188
+ : WithoutComp<T, C, readonly [...Acc, H]>
189
+ : Acc
190
+
191
+ /** Unions one pair into the component list: create, extend, keep, or merge. */
192
+ type AddPair<Comps extends readonly string[], A extends string, B extends string> = [
193
+ CompOf<Comps, A>,
194
+ CompOf<Comps, B>
195
+ ] extends [infer CA extends string, infer CB extends string]
196
+ ? [CA] extends [never]
197
+ ? [CB] extends [never]
198
+ ? readonly [...Comps, A | B]
199
+ : readonly [...WithoutComp<Comps, CB>, CB | A]
200
+ : [CB] extends [never]
201
+ ? readonly [...WithoutComp<Comps, CA>, CA | B]
202
+ : [CA, CB] extends [CB, CA]
203
+ ? Comps
204
+ : readonly [...WithoutComp<WithoutComp<Comps, CA>, CB>, CA | CB]
205
+ : Comps
206
+
207
+ /** Folds every pair into connected components (tail-recursive — the whole walk is one loop). */
208
+ type BuildComps<Pairs extends PairList, Comps extends readonly string[] = readonly []> = Pairs extends readonly [
209
+ infer P extends Pair,
210
+ ...infer T extends PairList
211
+ ]
212
+ ? BuildComps<T, AddPair<Comps, P[0], P[1]>>
213
+ : Comps
214
+
215
+ // ————————————————————————————————————————————————————————————————————————
216
+ // The wall and the names.
217
+ // ————————————————————————————————————————————————————————————————————————
218
+
219
+ /** Whether a union holds two or more members (`All` captures the whole union across distribution). */
220
+ type IsMulti<U, All = U> = [U] extends [never] ? false : U extends unknown ? ([All] extends [U] ? false : true) : never
221
+
222
+ /**
223
+ * The named, self-locating compile verdict of the one-generator wall: the
224
+ * generator coordinates that collided and the paired slots (rendered
225
+ * `A.x ~ B.y`, statement order) whose chain unified them. Intersected into
226
+ * `schema()`'s statements parameter, so the error lands ON the statement
227
+ * list with this key naming the law. The runtime twin throws from
228
+ * `computeClasses` with the same content, naming the exact statement.
229
+ */
230
+ interface ClassWall<Generators extends string, Chain extends readonly string[]> {
231
+ readonly "schema class wall — the statements unify two generators into one class (two mints cannot share a carrier)": {
232
+ readonly generators: Generators
233
+ readonly through: Chain
234
+ }
235
+ }
236
+
237
+ /** The paired slots lying inside component `C`, rendered — the wall's self-locating chain. */
238
+ type ChainOf<Pairs extends PairList, C extends string, Acc extends readonly string[] = []> = Pairs extends readonly [
239
+ infer P extends Pair,
240
+ ...infer T extends PairList
241
+ ]
242
+ ? [P[0]] extends [C]
243
+ ? ChainOf<T, C, readonly [...Acc, `${P[0]} ~ ${P[1]}`]>
244
+ : ChainOf<T, C, Acc>
245
+ : Acc
246
+
247
+ /** Scans the components for a two-generator class: `unknown` (lawful) or the {@link ClassWall}. */
248
+ type WallScan<Comps extends readonly string[], Gens extends string, Pairs extends PairList> = Comps extends readonly [
249
+ infer H extends string,
250
+ ...infer T extends readonly string[]
251
+ ]
252
+ ? true extends IsMulti<Extract<H, Gens>>
253
+ ? ClassWall<Extract<H, Gens>, ChainOf<Pairs, H>>
254
+ : WallScan<T, Gens, Pairs>
255
+ : unknown
256
+
257
+ /**
258
+ * The one-generator-per-class law as a constraint: resolves to `unknown`
259
+ * (a no-op intersection into the statements parameter) when the statement
260
+ * list is lawful, and to the named {@link ClassWall} otherwise.
261
+ */
262
+ type LawfulStatements<Rels extends SchemaRelations, Stmts extends readonly Statement[]> = WallScan<
263
+ BuildComps<PairsOf<Stmts>>,
264
+ GeneratorsOf<Rels>,
265
+ PairsOf<Stmts>
266
+ >
267
+
268
+ /**
269
+ * One coordinate's class per the three laws, at the TYPE tier: its
270
+ * component's single generator (the exact literal — a generator names its
271
+ * class); a component-less generator is its own class; a component-less
272
+ * non-generator is bare (`undefined`); and a GENERATOR-LESS component is
273
+ * carried as its member-coordinate SET (the union of the component's
274
+ * coordinates — a canonical, deterministic type). The set REPRESENTS the
275
+ * runtime's least-member class name faithfully: the runtime name is by
276
+ * construction a member, two slots share a class exactly when their sets
277
+ * are identical (so `JoinOk` judges identically at both tiers), and bare
278
+ * never equals a set. The least-member PICK itself is deliberately not
279
+ * made at the type tier: TypeScript's union member order is not observably
280
+ * deterministic (the same key union tuples differently across checking
281
+ * contexts — measured, not conjectured), so any type-level "least in
282
+ * declaration order" would drift between compilations; the ratified
283
+ * declaration-order name lives at the VALUE tier (`computeClasses`), which
284
+ * is the only tier the wire reads.
285
+ */
286
+ type ClassOfCoord<Comps extends readonly string[], Gens extends string, C extends string> = [CompOf<Comps, C>] extends [
287
+ infer M extends string
288
+ ]
289
+ ? [M] extends [never]
290
+ ? [C] extends [Gens]
291
+ ? C
292
+ : undefined
293
+ : [Extract<M, Gens>] extends [infer G extends string]
294
+ ? [G] extends [never]
295
+ ? M
296
+ : G
297
+ : never
298
+ : never
299
+
300
+ /** The computed class map over precomputed components/generators. */
301
+ type ComputedClasses<Rels extends SchemaRelations, Comps extends readonly string[], Gens extends string> = {
302
+ readonly [N in keyof Rels & string]: {
303
+ readonly [F in MemberFieldNames<Rels[N]>]: ClassOfCoord<Comps, Gens, `${N}.${F}`>
304
+ }
305
+ }
306
+
307
+ /**
308
+ * THE type-level class map of a schema: relation name → field name → the
309
+ * law-computed class (`undefined` = bare) — what `schema()` returns as the
310
+ * `classes` property's type, and what query joins compare. Generator
311
+ * classes are exact name literals; generator-less classes are their
312
+ * member-coordinate sets (see {@link ClassOfCoord} — the runtime map's
313
+ * least-member name is always a member, so the property type is honest).
314
+ */
315
+ type ClassesOf<Rels extends SchemaRelations, Stmts extends readonly Statement[]> = ComputedClasses<
316
+ Rels,
317
+ BuildComps<PairsOf<Stmts>>,
318
+ GeneratorsOf<Rels>
319
+ >
320
+
321
+ // ————————————————————————————————————————————————————————————————————————
322
+ // The runtime twin: the same computation as a plain union-find.
323
+ // ————————————————————————————————————————————————————————————————————————
324
+
325
+ /** One relation's declared coordinates and generator flags, in declaration order. */
326
+ interface MemberCoords {
327
+ readonly relation: string
328
+ readonly fields: ReadonlyArray<{ readonly name: string; readonly generator: boolean }>
329
+ }
330
+
331
+ /** Reads every member's coordinates off the relation record, declaration order throughout. */
332
+ function memberCoords(relations: SchemaRelations): MemberCoords[] {
333
+ const out: MemberCoords[] = []
334
+ for (const [relationName, member] of Object.entries(relations)) {
335
+ if ("handles" in member.data) {
336
+ const fields = [
337
+ { name: "id", generator: true },
338
+ ...member.data.columns.map(function columnCoord(column) {
339
+ return { name: column.name, generator: false }
340
+ })
341
+ ]
342
+ out.push({ relation: relationName, fields })
343
+ continue
344
+ }
345
+ const fields = member.data.fields.map(function fieldCoord(declared) {
346
+ return { name: declared.name, generator: "fresh" in declared.field && declared.field.fresh === true }
347
+ })
348
+ out.push({ relation: relationName, fields })
349
+ }
350
+ return out
351
+ }
352
+
353
+ /** A plain union-find over coordinate strings, with per-root generator rosters. */
354
+ interface UnionFind {
355
+ find(coord: string): string
356
+ union(a: string, b: string): string
357
+ generatorsOf(root: string): readonly string[]
358
+ markGenerator(coord: string): void
359
+ }
360
+
361
+ /** Builds the union-find. */
362
+ function makeUnionFind(): UnionFind {
363
+ const parent = new Map<string, string>()
364
+ const generators = new Map<string, string[]>()
365
+ function find(coord: string): string {
366
+ const at = parent.get(coord)
367
+ if (at === undefined) {
368
+ parent.set(coord, coord)
369
+ return coord
370
+ }
371
+ if (at === coord) {
372
+ return coord
373
+ }
374
+ const root = find(at)
375
+ parent.set(coord, root)
376
+ return root
377
+ }
378
+ return {
379
+ find,
380
+ union(a, b) {
381
+ const rootA = find(a)
382
+ const rootB = find(b)
383
+ if (rootA === rootB) {
384
+ return rootA
385
+ }
386
+ parent.set(rootB, rootA)
387
+ const merged = [...(generators.get(rootA) ?? []), ...(generators.get(rootB) ?? [])]
388
+ generators.delete(rootB)
389
+ if (merged.length > 0) {
390
+ generators.set(rootA, merged)
391
+ }
392
+ return rootA
393
+ },
394
+ generatorsOf(root) {
395
+ return generators.get(root) ?? []
396
+ },
397
+ markGenerator(coord) {
398
+ const root = find(coord)
399
+ generators.set(root, [...(generators.get(root) ?? []), coord])
400
+ }
401
+ }
402
+ }
403
+
404
+ /** The paired faces of one statement, or undefined for a key (an FD pairs nothing). */
405
+ function statementFaces(statement: Statement): readonly [FaceData, FaceData] | undefined {
406
+ const data = statement.data
407
+ if (data.kind === "key") {
408
+ return undefined
409
+ }
410
+ return [data.source, data.target]
411
+ }
412
+
413
+ /**
414
+ * Computes the class map — the runtime twin of {@link ClassesOf}, the SAME
415
+ * computation as a plain union-find: every paired face's positionwise slot
416
+ * pairs union their coordinates; a fresh field or closed id is a generator
417
+ * naming its class; a generator-less class is named by its least member in
418
+ * relation-declaration × field-declaration order; a slot in no law is bare
419
+ * (`undefined`). The one-generator wall throws HERE, naming the two
420
+ * coordinates and the statement that unified them — the same content the
421
+ * compile-tier {@link ClassWall} carries. The returned map is frozen, own
422
+ * properties throughout (arbitrary field names ride own-property
423
+ * definition, never the object protocol).
424
+ */
425
+ function computeClasses(name: string, relations: SchemaRelations, statements: readonly Statement[]): SchemaClasses {
426
+ const members = memberCoords(relations)
427
+ const uf = makeUnionFind()
428
+ const paired = new Set<string>()
429
+ const generatorSet = new Set<string>()
430
+ for (const member of members) {
431
+ for (const field of member.fields) {
432
+ const coord = `${member.relation}.${field.name}`
433
+ uf.find(coord)
434
+ if (field.generator) {
435
+ generatorSet.add(coord)
436
+ uf.markGenerator(coord)
437
+ }
438
+ }
439
+ }
440
+ for (const statement of statements) {
441
+ const faces = statementFaces(statement)
442
+ if (faces === undefined) {
443
+ continue
444
+ }
445
+ const [source, target] = faces
446
+ source.projection.forEach(function unionSlot(fieldName, position) {
447
+ const targetField = target.projection[position]
448
+ if (targetField === undefined) {
449
+ return
450
+ }
451
+ const coordA = `${source.owner.name}.${fieldName}`
452
+ const coordB = `${target.owner.name}.${targetField}`
453
+ paired.add(coordA)
454
+ paired.add(coordB)
455
+ const root = uf.union(coordA, coordB)
456
+ const gens = uf.generatorsOf(root)
457
+ if (gens.length > 1) {
458
+ throw errors.new(
459
+ `schema ${name}: the statements unify two generators into one class — ${gens.join(" and ")} (two mints cannot share a carrier) — ${renderStatement(statement)}`
460
+ )
461
+ }
462
+ })
463
+ }
464
+ const names = new Map<string, string>()
465
+ for (const member of members) {
466
+ for (const field of member.fields) {
467
+ const coord = `${member.relation}.${field.name}`
468
+ const root = uf.find(coord)
469
+ if (!names.has(root)) {
470
+ const gens = uf.generatorsOf(root)
471
+ names.set(root, gens[0] ?? coord)
472
+ }
473
+ }
474
+ }
475
+ const classes: Record<string, RelationClasses> = {}
476
+ for (const member of members) {
477
+ const record: Record<string, string | undefined> = {}
478
+ for (const field of member.fields) {
479
+ const coord = `${member.relation}.${field.name}`
480
+ const classed = paired.has(coord) || generatorSet.has(coord)
481
+ Object.defineProperty(record, field.name, {
482
+ value: classed ? names.get(uf.find(coord)) : undefined,
483
+ enumerable: true
484
+ })
485
+ }
486
+ Object.freeze(record)
487
+ Object.defineProperty(classes, member.relation, { value: record, enumerable: true })
488
+ }
489
+ return Object.freeze(classes)
490
+ }
491
+
492
+ /**
493
+ * The trusted seam of the class-map mint (the `refsComplete` pattern): the
494
+ * checkable facts — one own record per declared relation, one own entry
495
+ * per declared field (the closed sealed shape's `id` included), everything
496
+ * frozen — are verified before the runtime map is admitted at the computed
497
+ * {@link ClassesOf} type. The NAME agreement of the two tiers is pinned by
498
+ * the generated fixture probes (the runtime/type diff check).
499
+ */
500
+ function classesComplete<Classes extends SchemaClasses>(
501
+ classes: SchemaClasses,
502
+ relations: SchemaRelations
503
+ ): classes is Classes {
504
+ if (!Object.isFrozen(classes)) {
505
+ return false
506
+ }
507
+ return memberCoords(relations).every(function relationMinted(member) {
508
+ const record = classes[member.relation]
509
+ if (record === undefined || !Object.isFrozen(record)) {
510
+ return false
511
+ }
512
+ return member.fields.every(function fieldMinted(field) {
513
+ return Object.hasOwn(record, field.name)
514
+ })
515
+ })
516
+ }
517
+
518
+ export type { ClassesOf, ClassLookup, ClassRecordOf, ClassWall, LawfulStatements, RelationClasses, SchemaClasses }
519
+ export { classesComplete, computeClasses }
package/src/lower.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  import type { AnyClosed } from "#closed.ts"
12
12
  import type { FaceData } from "#face.ts"
13
13
  import type { AnyField } from "#fields.ts"
14
+ import type { RelationClasses } from "#law.ts"
14
15
  import type { AnyRelation } from "#relation.ts"
15
16
  import type { AnySchema, SchemaRelation } from "#schema.ts"
16
17
  import type {
@@ -57,17 +58,19 @@ function valueTypeOf(field: AnyField): ValueTypeSpec {
57
58
 
58
59
  /**
59
60
  * Lowers one field descriptor to its {@link FieldSpec}: the structural
60
- * type, the DOMAIN label as the wire's `newtype` (the macro's `as NewType`
61
- * namecarried for engine handle resolution, dropped at descriptor
62
- * lowering, never fingerprinted), and the structural fresh mark (`fresh`
63
- * is the literal `true` exactly on a fresh-marked u64 on an unmarked one
64
- * the property holds the marked descriptor, never `true`).
61
+ * type, the structural fresh mark (`fresh` is the literal `true` exactly
62
+ * on a fresh-marked u64 on an unmarked one the property holds the marked
63
+ * descriptor, never `true`), and the wire's `newtype` the COMPUTED class
64
+ * name `schema()` derived from the statement list (the laws type the
65
+ * columns), `undefined` on a bare field. The engine reads newtypes for
66
+ * handle resolution and the coherence check only and DROPS them at
67
+ * descriptor lowering — class names are never fingerprinted.
65
68
  */
66
- function lowerField(name: string, field: AnyField): FieldSpec {
69
+ function lowerField(name: string, field: AnyField, newtype: string | undefined): FieldSpec {
67
70
  return {
68
71
  name,
69
72
  valueType: valueTypeOf(field),
70
- newtype: field.domain,
73
+ newtype,
71
74
  fresh: "fresh" in field && field.fresh === true
72
75
  }
73
76
  }
@@ -113,11 +116,14 @@ function lowerStatement(statement: Statement): StatementSpec {
113
116
 
114
117
  /**
115
118
  * Lowers one ordinary relation to its `RelationSpec` fragment: fields in
116
- * declaration order, `extension: undefined` (the option is the kind).
119
+ * declaration order, each carrying its law-computed class name as the
120
+ * `newtype` (`classes` — the schema's class record for this relation;
121
+ * bare fields carry `undefined`), `extension: undefined` (the option is
122
+ * the kind).
117
123
  */
118
- function lowerRelation(relation: AnyRelation): RelationSpec {
124
+ function lowerRelation(relation: AnyRelation, classes: RelationClasses): RelationSpec {
119
125
  const fields: FieldSpec[] = relation.data.fields.map(function lowerDeclared(declared) {
120
- return lowerField(declared.name, declared.field)
126
+ return lowerField(declared.name, declared.field, classes[declared.name])
121
127
  })
122
128
  return { name: relation.name, newtype: undefined, fields, extension: undefined }
123
129
  }
@@ -125,34 +131,41 @@ function lowerRelation(relation: AnyRelation): RelationSpec {
125
131
  /**
126
132
  * Lowers one closed relation to its `RelationSpec` fragment: declared
127
133
  * intrinsic columns only (the engine materializes the synthetic `id`),
128
- * the HANDLE DOMAIN as its handle newtype the `id` descriptor's own
129
- * `` `${name}Id` `` label (Rust's `closed relation Kind as KindId`), the
130
- * same string every referencing field carries, which is exactly how the
131
- * engine resolves a handle literal back to its roster — and the ground
132
- * axioms in declaration order (row id = index); the literals were already
133
- * lowered at `closed()` construction.
134
+ * the handle newtype the COMPUTED class name of the id's generator
135
+ * class (`"Kind.id"`, always present: a closed id is a generator), which
136
+ * every referencing field shares by law (how the engine resolves a handle
137
+ * literal back to its roster) — and the ground axioms in declaration
138
+ * order (row id = index); the literals were already lowered at `closed()`
139
+ * construction.
134
140
  */
135
- function lowerClosed(member: AnyClosed): RelationSpec {
141
+ function lowerClosed(member: AnyClosed, classes: RelationClasses): RelationSpec {
136
142
  const fields: FieldSpec[] = member.data.columns.map(function lowerColumn(column) {
137
- return lowerField(column.name, column.field)
143
+ return lowerField(column.name, column.field, classes[column.name])
138
144
  })
139
145
  const extension = member.data.rows.map(function lowerRow(row) {
140
146
  return { handle: row.handle, values: row.values }
141
147
  })
142
- return { name: member.name, newtype: member.id.domain, fields, extension }
148
+ return { name: member.name, newtype: classes.id, fields, extension }
143
149
  }
144
150
 
151
+ /** The frozen empty class record a relation outside the schema's map lowers under (nothing classed). */
152
+ const noClasses: RelationClasses = Object.freeze({})
153
+
145
154
  /**
146
155
  * Lowers a whole theory to the `SchemaSpec` the bridge takes: relations in
147
156
  * record declaration order, DECLARED statements only in written order (the
148
- * engine materializes the fresh-implied and closed auto-keys itself).
157
+ * engine materializes the fresh-implied and closed auto-keys itself), and
158
+ * every field's `newtype` slot fed from the schema's law-computed class
159
+ * map — the ONE domain authority (fingerprint-neutral: the engine drops
160
+ * newtypes at descriptor lowering).
149
161
  */
150
162
  function lower(theory: AnySchema): SchemaSpec {
151
- const relations: RelationSpec[] = Object.values(theory.relations).map(function lowerMember(member) {
163
+ const relations: RelationSpec[] = Object.entries(theory.relations).map(function lowerMember([name, member]) {
164
+ const classes = theory.classes[name] ?? noClasses
152
165
  if (isClosedMember(member)) {
153
- return lowerClosed(member)
166
+ return lowerClosed(member, classes)
154
167
  }
155
- return lowerRelation(member)
168
+ return lowerRelation(member, classes)
156
169
  })
157
170
  return { relations, statements: theory.statements.map(lowerStatement) }
158
171
  }
package/src/native.ts CHANGED
@@ -244,13 +244,17 @@ interface Violation {
244
244
  * `dbCreate`/`dbOpen`'s domain outcome. `schemaError` covers both spec
245
245
  * resolution (unresolvable names, banned spellings — every issue in one
246
246
  * message) and schema validation at the declaration boundary;
247
- * `fingerprintMismatch` is `dbOpen`'s stored-theory refusal.
247
+ * `newtypeMismatch` is the coherence wall's own kind — a spec whose
248
+ * statement pairs faces with disagreeing newtype labels (the engine twin
249
+ * of the schema-level class wall; unreachable through the typed builder,
250
+ * which computes every label from the laws, so only a raw spec can reach
251
+ * it); `fingerprintMismatch` is `dbOpen`'s stored-theory refusal.
248
252
  */
249
253
  type DbOpenResult =
250
254
  | { readonly ok: true; readonly db: DbHandle }
251
255
  | {
252
256
  readonly ok: false
253
- readonly kind: "schemaError" | "fingerprintMismatch"
257
+ readonly kind: "schemaError" | "newtypeMismatch" | "fingerprintMismatch"
254
258
  readonly message: string
255
259
  }
256
260