@bjornpagen/bumbledb 0.1.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 (112) hide show
  1. package/COOKBOOK.md +1450 -0
  2. package/README.md +69 -30
  3. package/dist/closed.d.ts +157 -38
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +232 -47
  6. package/dist/closed.js.map +1 -1
  7. package/dist/count.d.ts +64 -17
  8. package/dist/count.d.ts.map +1 -1
  9. package/dist/count.js +37 -14
  10. package/dist/count.js.map +1 -1
  11. package/dist/db.d.ts +27 -8
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +33 -10
  14. package/dist/db.js.map +1 -1
  15. package/dist/exhume.d.ts +12 -11
  16. package/dist/exhume.d.ts.map +1 -1
  17. package/dist/exhume.js +7 -6
  18. package/dist/exhume.js.map +1 -1
  19. package/dist/face.d.ts +135 -30
  20. package/dist/face.d.ts.map +1 -1
  21. package/dist/face.js +44 -19
  22. package/dist/face.js.map +1 -1
  23. package/dist/fields.d.ts +130 -130
  24. package/dist/fields.d.ts.map +1 -1
  25. package/dist/fields.js +60 -94
  26. package/dist/fields.js.map +1 -1
  27. package/dist/index.d.ts +29 -24
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +19 -14
  30. package/dist/index.js.map +1 -1
  31. package/dist/law.d.ts +224 -0
  32. package/dist/law.d.ts.map +1 -0
  33. package/dist/law.js +224 -0
  34. package/dist/law.js.map +1 -0
  35. package/dist/lower.d.ts +17 -7
  36. package/dist/lower.d.ts.map +1 -1
  37. package/dist/lower.js +66 -24
  38. package/dist/lower.js.map +1 -1
  39. package/dist/marshal.d.ts +36 -21
  40. package/dist/marshal.d.ts.map +1 -1
  41. package/dist/marshal.js +41 -25
  42. package/dist/marshal.js.map +1 -1
  43. package/dist/native.d.ts +6 -2
  44. package/dist/native.d.ts.map +1 -1
  45. package/dist/native.js +2 -2
  46. package/dist/native.js.map +1 -1
  47. package/dist/query/atom.d.ts +370 -174
  48. package/dist/query/atom.d.ts.map +1 -1
  49. package/dist/query/atom.js +75 -172
  50. package/dist/query/atom.js.map +1 -1
  51. package/dist/query/lower.d.ts +312 -109
  52. package/dist/query/lower.d.ts.map +1 -1
  53. package/dist/query/lower.js +906 -387
  54. package/dist/query/lower.js.map +1 -1
  55. package/dist/query/predicate.d.ts +70 -88
  56. package/dist/query/predicate.d.ts.map +1 -1
  57. package/dist/query/predicate.js +118 -72
  58. package/dist/query/predicate.js.map +1 -1
  59. package/dist/query/run.d.ts +19 -16
  60. package/dist/query/run.d.ts.map +1 -1
  61. package/dist/query/run.js +31 -24
  62. package/dist/query/run.js.map +1 -1
  63. package/dist/query/scope.d.ts +178 -127
  64. package/dist/query/scope.d.ts.map +1 -1
  65. package/dist/query/scope.js +111 -108
  66. package/dist/query/scope.js.map +1 -1
  67. package/dist/query/select.d.ts +102 -80
  68. package/dist/query/select.d.ts.map +1 -1
  69. package/dist/query/select.js +39 -34
  70. package/dist/query/select.js.map +1 -1
  71. package/dist/relation.d.ts +48 -38
  72. package/dist/relation.d.ts.map +1 -1
  73. package/dist/relation.js +24 -19
  74. package/dist/relation.js.map +1 -1
  75. package/dist/schema.d.ts +41 -3
  76. package/dist/schema.d.ts.map +1 -1
  77. package/dist/schema.js +34 -9
  78. package/dist/schema.js.map +1 -1
  79. package/dist/spec.d.ts +7 -5
  80. package/dist/spec.d.ts.map +1 -1
  81. package/dist/spec.js.map +1 -1
  82. package/dist/statements.d.ts +91 -36
  83. package/dist/statements.d.ts.map +1 -1
  84. package/dist/statements.js +42 -12
  85. package/dist/statements.js.map +1 -1
  86. package/package.json +4 -3
  87. package/src/closed.ts +448 -95
  88. package/src/count.ts +112 -18
  89. package/src/db.ts +45 -17
  90. package/src/exhume.ts +12 -11
  91. package/src/face.ts +211 -41
  92. package/src/fields.ts +199 -234
  93. package/src/index.ts +78 -61
  94. package/src/law.ts +519 -0
  95. package/src/lower.ts +82 -27
  96. package/src/marshal.ts +48 -30
  97. package/src/native.ts +8 -4
  98. package/src/query/atom.ts +525 -374
  99. package/src/query/lower.ts +1469 -538
  100. package/src/query/predicate.ts +208 -161
  101. package/src/query/run.ts +35 -25
  102. package/src/query/scope.ts +264 -218
  103. package/src/query/select.ts +168 -93
  104. package/src/relation.ts +47 -47
  105. package/src/schema.ts +69 -18
  106. package/src/spec.ts +7 -5
  107. package/src/statements.ts +124 -41
  108. package/dist/brand.d.ts +0 -59
  109. package/dist/brand.d.ts.map +0 -1
  110. package/dist/brand.js +0 -47
  111. package/dist/brand.js.map +0 -1
  112. package/src/brand.ts +0 -82
package/src/closed.ts CHANGED
@@ -1,75 +1,89 @@
1
1
  /**
2
2
  * Closed relations (`docs/architecture/10-data-model.md` § closed
3
3
  * relations): a vocabulary whose extension is declared in the schema — two
4
- * tiers, one function. The emission per closed relation mirrors the macro's
5
- * (host-enum analog): handle CONSTANTS on the value (`Kind.Checking`, ids =
6
- * declaration order), the `fromId` weld, an `id` field constructor
7
- * pre-branded with the handle newtype for other relations' field blocks
8
- * (`kind: Kind.id`), and payload readback (`Kind.axioms`). No fact type and
9
- * no insert surface exist closed relations are unwritable by
10
- * construction: the value simply lacks the writable relation shape.
4
+ * tiers, one function. The emission per closed relation mirrors the
5
+ * macro's (host-enum analog): handle CONSTANTS on the value
6
+ * (`Kind.Checking`, ids = declaration order, each a BARE `bigint` no
7
+ * brand), the `fromId` weld, an `id` field descriptor carrying the CLOSED
8
+ * LINKAGE (the roster — pure structure, no declared domain: the laws type
9
+ * the columns, and `schema()` names the id's generator class `"Kind.id"`)
10
+ * for other relations' field blocks (`kind: Kind.id`), payload readback
11
+ * (`Kind.axioms`), and the declared payload column descriptors
12
+ * (`Kind.columns` — the runtime twin of the `Cols` type parameter, which
13
+ * the face layer's structural wall reads). Bare tier: `closed("Kind", ["Checking",
14
+ * "Savings"])`. Payload tier: `closed("Sev", { pages: bool }, { Critical:
15
+ * { pages: true }, ... })` — one call, three arguments (the curried tier-2
16
+ * spelling is DELETED — canonical utterance): the axioms record IS the
17
+ * handle declaration, every handle carrying every column exactly once
18
+ * (type-enforced). No fact type and no insert surface exist — closed
19
+ * relations are unwritable by construction: the value simply lacks the
20
+ * writable relation shape. Both tiers mint `match()` — exhaustive dispatch
21
+ * over the handle union (arms typed by the mapped type, so a missing or
22
+ * extra arm is a compile error; the payload tier's arm receives the typed
23
+ * axiom row). The payload tier additionally mints `where()` — the
24
+ * ψ-selection surface (`Kind.where({ mastered: true })` as a face source),
25
+ * resolved through the ONE selection machine
26
+ * (`relation.ts::resolveSelection`); the bare tier has no payload columns to
27
+ * select on, so `.where` is absent there, at the type AND on the value.
11
28
  */
12
29
 
13
30
  import * as errors from "@superbuilders/errors"
14
- import type { Brand } from "#brand.ts"
31
+ import type { OneOf } from "#face.ts"
15
32
  import {
16
33
  type AnyField,
17
34
  assertDeclarationOrderKey,
18
35
  type ClosedIdField,
19
36
  type ClosedRoster,
20
- type FieldData,
21
- type FieldValue,
22
- fieldData,
37
+ type Infer,
23
38
  literalOf
24
39
  } from "#fields.ts"
40
+ import { resolveSelection, type SelectionBinding } from "#relation.ts"
25
41
  import type { LiteralSpec } from "#spec.ts"
26
42
 
27
43
  /**
28
44
  * The value-surface property names a handle may not shadow — the macro's
29
45
  * name-collision diagnostic, here over the closed value's own properties
30
46
  * (`relation`/`selection` are reserved so a closed value can never be
31
- * mistaken for a selected relation by `on()`'s discriminant).
47
+ * mistaken for a selected relation by `on()`'s discriminant; `where` is
48
+ * reserved because the payload tier mints the ψ-selection method under
49
+ * exactly that name; `match` because BOTH tiers mint the exhaustive
50
+ * dispatch under exactly that name).
32
51
  */
33
52
  const reservedHandleNames: readonly string[] = Object.freeze([
34
53
  "name",
35
54
  "id",
36
55
  "data",
37
56
  "axioms",
57
+ "columns",
38
58
  "fromId",
59
+ "where",
60
+ "match",
39
61
  "relation",
40
62
  "selection"
41
63
  ])
42
64
 
43
65
  /**
44
- * Reads one handle's axiom row, refusing absence loudly: the payload tier's
45
- * overload types the record exhaustively, so a missing row is ill-typed
46
- * input, and the bare tier never reaches here (it declares no columns).
66
+ * A payload column of a closed relation: any field descriptor except a
67
+ * fresh-marked one (a vocabulary's rows are ground axioms, never minted).
47
68
  */
48
- function axiomRow(
49
- name: string,
50
- axioms: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined,
51
- handle: string
52
- ): Readonly<Record<string, unknown>> {
53
- if (axioms === undefined) {
54
- throw errors.new(`closed relation ${name}: payload columns declared without ground axioms`)
55
- }
56
- const row = axioms[handle]
57
- if (row === undefined) {
58
- throw errors.new(`closed relation ${name}: no ground axiom for handle ${handle}`)
59
- }
60
- return row
61
- }
69
+ type PayloadField = Exclude<AnyField, { readonly fresh: true }>
62
70
 
63
71
  /**
64
- * A payload column of a closed relation: any field constructor except a
65
- * fresh-marked one (a vocabulary's rows are ground axioms, never minted).
72
+ * A declared payload column BLOCK: name descriptor, with `id`
73
+ * unspellable the sealed shape mints the synthetic `id` itself (ordinal
74
+ * 0 of the matchable fields), so a declared column named `id` would be
75
+ * shadowed by the synthetic slot everywhere the shape resolves by name
76
+ * (`matchFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
77
+ * typed here and judged again at construction in {@link mintClosed} — the
78
+ * runtime twin for untyped callers, warmer and earlier than the engine's
79
+ * `DuplicateFieldName` at `Db.create`.
66
80
  */
67
- type PayloadField = AnyField & { readonly data: FieldData<false> }
81
+ type PayloadColumns = Record<string, PayloadField> & { readonly id?: never }
68
82
 
69
- /** One declared payload column: name plus its field description. */
83
+ /** One declared payload column: name plus its field descriptor. */
70
84
  interface ClosedColumn {
71
85
  readonly name: string
72
- readonly field: FieldData
86
+ readonly field: PayloadField
73
87
  }
74
88
 
75
89
  /**
@@ -91,75 +105,363 @@ interface ClosedData {
91
105
  readonly rows: readonly ClosedRow[]
92
106
  }
93
107
 
94
- /** One axiom row as the host writes and reads it: column name to host value. */
95
- type AxiomRow<Cols> = { readonly [C in keyof Cols]: FieldValue<Cols[C]> }
108
+ /** One axiom row as the host writes and reads it: column name to bare structural value. */
109
+ type AxiomRow<Cols extends Record<string, PayloadField>> = { readonly [C in keyof Cols]: Infer<Cols[C]> }
96
110
 
97
111
  /**
98
112
  * The whole axiom record: every handle exactly once, every column exactly
99
- * once per row — a missing or extra axiom, column, or handle is a TYPE
100
- * error (mapped over the handle tuple).
113
+ * once per row — a missing or extra column is a TYPE error (each row is
114
+ * contextually checked against the declared columns), and the handle set
115
+ * IS the record's key set.
101
116
  */
102
- type Axioms<Handles extends readonly string[], Cols> = {
103
- readonly [H in Handles[number]]: AxiomRow<Cols>
117
+ type Axioms<Handles extends string, Cols extends Record<string, PayloadField>> = {
118
+ readonly [H in Handles]: AxiomRow<Cols>
104
119
  }
105
120
 
106
121
  /**
107
122
  * The named surface of a closed relation value, minus the handle constants
108
123
  * (which {@link Closed} intersects in).
109
124
  */
110
- interface ClosedCore<Name extends string, Handles extends readonly string[], Cols> {
125
+ interface ClosedCore<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
111
126
  readonly name: Name
112
127
  /**
113
- * The pre-branded u64 field constructor: `kind: Kind.id` in another
114
- * relation's field block is the reference through which bare handles
115
- * become legal in that relation's selections.
128
+ * The closed reference descriptor: `kind: Kind.id` in another relation's
129
+ * field block is the reference through which bare handle ids become
130
+ * legal in that relation's selections. Pure structure plus the roster —
131
+ * the referencing field's domain is law-born: `schema()` computes it
132
+ * from the declared containment (`"Kind.id"`, the generator class).
116
133
  */
117
- readonly id: ClosedIdField<Name>
134
+ readonly id: ClosedIdField
118
135
  readonly data: ClosedData
119
- /** Payload readback: handle to its declared column values. */
136
+ /** Payload readback: handle to its declared column values, bare and structural. */
120
137
  readonly axioms: Axioms<Handles, Cols>
138
+ /**
139
+ * The declared payload columns, name → field descriptor — an HONEST
140
+ * frozen runtime record (the descriptors themselves, by identity), and
141
+ * the typed carrier a projected payload column's structural shape is
142
+ * recovered through off the schema type (the face layer's
143
+ * `ProjectedShape` reads it; `data.columns` carries the same
144
+ * descriptors in declaration order for the lowering).
145
+ */
146
+ readonly columns: Cols
121
147
  /** The weld: declaration-order id back to its handle, or undefined beyond the roster. */
122
- fromId(id: Brand<bigint, Name>): Handles[number] | undefined
148
+ fromId(id: bigint): Handles | undefined
149
+ }
150
+
151
+ /**
152
+ * The `where()` argument of a closed relation: per PAYLOAD column, a bare
153
+ * structural literal of that column's value type or an `oneOf(a, b, ...)`
154
+ * literal set — the ordinary `where()`'s vocabulary exactly. The synthetic
155
+ * `id` is deliberately absent: an id selection is spelled only as handle
156
+ * literals on the REFERENCING side (the canonical-utterance law).
157
+ */
158
+ type ClosedSelectionInput<Cols extends Record<string, PayloadField>> = {
159
+ readonly [C in keyof Cols]?: Infer<Cols[C]> | OneOf<Infer<Cols[C]>>
123
160
  }
124
161
 
125
162
  /**
126
- * A closed relation value: the core surface plus one branded constant per
127
- * handle (`Kind.Checking: Brand<bigint, "Kind">`, ids = declaration order).
163
+ * A closed relation with a ψ selection applied what `on()` consumes as a
164
+ * σ-carrying closed source (`on(Kind.where({ mastered: true }), "id")`).
165
+ * Deliberately the SAME discriminant shape as the ordinary `Selected`
166
+ * (`relation`/`selection` — `face.ts::faceParts` splits both by `"relation"
167
+ * in source`), and structurally UNMISTAKABLE for one: an `AnyClosed` lacks
168
+ * the relation shape (no `fields` record, no `RelationData`).
128
169
  */
129
- type Closed<Name extends string, Handles extends readonly string[], Cols> = ClosedCore<Name, Handles, Cols> & {
130
- readonly [H in Handles[number]]: Brand<bigint, Name>
170
+ interface SelectedClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
171
+ readonly relation: Closed<Name, Handles, Cols>
172
+ readonly selection: readonly SelectionBinding[]
131
173
  }
132
174
 
175
+ /** Any ψ-selected closed relation value. */
176
+ interface AnySelectedClosed {
177
+ readonly relation: AnyClosed
178
+ readonly selection: readonly SelectionBinding[]
179
+ }
180
+
181
+ /**
182
+ * The ψ-selection surface of a payload-tier closed value. The selection is
183
+ * resolved EAGERLY against the declared columns and lowered as-is — the SDK
184
+ * never pre-folds ψ into an id set: pass-through lowering is what the macro
185
+ * does, and the ENGINE folds against the sealed extension at validate
186
+ * (`compile_member_set`).
187
+ */
188
+ interface ClosedSelectable<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
189
+ where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols>
190
+ }
191
+
192
+ /**
193
+ * Exhaustive dispatch over a BARE closed vocabulary: one arm per handle,
194
+ * no literal types and no brands — the handle-name union on the value's
195
+ * type IS the exhaustiveness proof (a missing arm is a missing-property
196
+ * compile error; an extra arm an excess-property compile error). The bare
197
+ * tier declares no payload, so an arm takes nothing. The arms record types
198
+ * any `bigint` in — the runtime roster refuses an out-of-vocabulary id
199
+ * with a throw, never a misdispatch.
200
+ */
201
+ interface ClosedMatchBare<Handles extends string> {
202
+ match<T>(id: bigint, arms: { readonly [H in Handles]: () => T }): T
203
+ }
204
+
205
+ /**
206
+ * Exhaustive dispatch over a PAYLOAD-tier closed vocabulary: the same
207
+ * mapped-type exhaustiveness as the bare tier, and each arm receives its
208
+ * handle's typed axiom row (the declared columns, bare and structural —
209
+ * the frozen readback row from `axioms`).
210
+ */
211
+ interface ClosedMatchPayload<Handles extends string, Cols extends Record<string, PayloadField>> {
212
+ match<T>(id: bigint, arms: { readonly [H in Handles]: (row: AxiomRow<Cols>) => T }): T
213
+ }
214
+
215
+ /**
216
+ * A closed relation value: the core surface plus one BARE constant per
217
+ * handle (`Kind.Checking: bigint`, ids = declaration order — the value is
218
+ * structural; the roster judges out-of-vocabulary ids at construction and
219
+ * the engine at commit), plus `match()` on BOTH tiers (bare arms take
220
+ * nothing; payload arms receive the typed axiom row), plus — exactly when
221
+ * payload columns exist — `where()` (the bare tier has nothing to select
222
+ * on, so the method is ABSENT there, not merely uncallable).
223
+ */
224
+ type Closed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> = ClosedCore<
225
+ Name,
226
+ Handles,
227
+ Cols
228
+ > & { readonly [H in Handles]: bigint } & ([keyof Cols] extends [never]
229
+ ? ClosedMatchBare<Handles>
230
+ : ClosedSelectable<Name, Handles, Cols> & ClosedMatchPayload<Handles, Cols>)
231
+
133
232
  /** Any closed relation value, whatever its roster and columns. */
134
233
  interface AnyClosed {
135
234
  readonly name: string
136
- readonly id: ClosedIdField<string>
235
+ readonly id: ClosedIdField
137
236
  readonly data: ClosedData
138
237
  readonly axioms: Readonly<Record<string, object>>
238
+ readonly columns: Readonly<Record<string, PayloadField>>
239
+ }
240
+
241
+ /** Narrows the two-tier second argument: a handle tuple (bare tier) or a column block (payload tier). */
242
+ function isHandleTuple(
243
+ shape: readonly [string, ...string[]] | Record<string, PayloadField>
244
+ ): shape is readonly [string, ...string[]] {
245
+ return Array.isArray(shape)
246
+ }
247
+
248
+ /**
249
+ * The trusted seam of the payload tier's handle enumeration: the axioms
250
+ * record's own enumerable keys ARE its handle set (the type says so —
251
+ * {@link Axioms} is keyed by the handles), and this guard verifies exactly
252
+ * that checkable fact before the key list is admitted at the handle type.
253
+ */
254
+ function handleKeysOwn<Handles extends string>(
255
+ axioms: { readonly [H in Handles]: object },
256
+ names: readonly string[]
257
+ ): names is readonly Handles[] {
258
+ return names.every(function ownHandle(name) {
259
+ return Object.hasOwn(axioms, name)
260
+ })
261
+ }
262
+
263
+ /**
264
+ * The trusted seam of the axiom-readback mint: every handle carries an own
265
+ * frozen row and every row carries every declared column as an own
266
+ * property — verified before the record is admitted as the typed
267
+ * {@link Axioms} (the `refsComplete` analog of `relation()`).
268
+ */
269
+ function axiomsMinted<Handles extends string, Cols extends Record<string, PayloadField>>(
270
+ record: Readonly<Record<string, object>>,
271
+ handles: readonly Handles[],
272
+ cols: readonly ClosedColumn[]
273
+ ): record is Axioms<Handles, Cols> & Readonly<Record<string, object>> {
274
+ return handles.every(function rowMinted(handle) {
275
+ const row = record[handle]
276
+ return (
277
+ row !== undefined &&
278
+ cols.every(function columnMinted(column) {
279
+ return Object.hasOwn(row, column.name)
280
+ })
281
+ )
282
+ })
283
+ }
284
+
285
+ /**
286
+ * The trusted seam of the handle-constant mint: every handle reads back as
287
+ * an own bigint — verified before the record is admitted at the constants
288
+ * type (a "__proto__"-named handle riding the object-protocol accessor
289
+ * would fail exactly this check).
290
+ */
291
+ function constantsMinted<Handles extends string>(
292
+ record: Readonly<Record<string, bigint>>,
293
+ handles: readonly Handles[]
294
+ ): record is Readonly<Record<string, bigint>> & { readonly [H in Handles]: bigint } {
295
+ return handles.every(function constantMinted(handle) {
296
+ return typeof record[handle] === "bigint"
297
+ })
298
+ }
299
+
300
+ /**
301
+ * Reads one handle's ground axiom row for lowering. The typed payload
302
+ * surface makes absence unrepresentable ({@link Axioms} carries every
303
+ * handle's row); the refusal below guards the one ill-typed path — payload
304
+ * columns with the bare tier's absent axioms — which no public spelling
305
+ * reaches.
306
+ */
307
+ function groundRow<Handles extends string, Cols extends Record<string, PayloadField>>(
308
+ name: string,
309
+ axioms: Axioms<Handles, Cols> | undefined,
310
+ handle: Handles
311
+ ): Readonly<Record<string, unknown>> {
312
+ if (axioms === undefined) {
313
+ throw errors.new(`closed relation ${name}: payload columns declared without ground axioms`)
314
+ }
315
+ return axioms[handle]
316
+ }
317
+
318
+ /**
319
+ * Mints the axiom-readback record: one own frozen row per handle (the bare
320
+ * tier's rows are empty — it declares no columns), each row a fresh copy of
321
+ * its ground axiom.
322
+ */
323
+ function mintAxioms<Handles extends string, Cols extends Record<string, PayloadField>>(
324
+ name: string,
325
+ handles: readonly Handles[],
326
+ cols: readonly ClosedColumn[],
327
+ axioms: Axioms<Handles, Cols> | undefined
328
+ ): Axioms<Handles, Cols> {
329
+ const out: Record<string, object> = {}
330
+ for (const handle of handles) {
331
+ const row = axioms === undefined ? Object.freeze({}) : Object.freeze({ ...groundRow(name, axioms, handle) })
332
+ Object.defineProperty(out, handle, { value: row, enumerable: true })
333
+ }
334
+ Object.freeze(out)
335
+ if (!axiomsMinted<Handles, Cols>(out, handles, cols)) {
336
+ throw errors.new(`closed relation ${name}: axiom-row minting incomplete`)
337
+ }
338
+ return out
339
+ }
340
+
341
+ /** Mints the handle constants: one own bigint per handle, ids = declaration order. */
342
+ function mintHandleConstants<Handles extends string>(
343
+ name: string,
344
+ handles: readonly Handles[]
345
+ ): { readonly [H in Handles]: bigint } {
346
+ const out: Record<string, bigint> = {}
347
+ handles.forEach(function mintHandleConstant(handle, index) {
348
+ Object.defineProperty(out, handle, { value: BigInt(index), enumerable: true })
349
+ })
350
+ Object.freeze(out)
351
+ if (!constantsMinted(out, handles)) {
352
+ throw errors.new(`closed relation ${name}: handle-constant minting incomplete`)
353
+ }
354
+ return out
139
355
  }
140
356
 
141
357
  /** Bare tier: `closed("Kind", ["Checking", "Savings"])` — handles only. */
142
358
  function closed<const Name extends string, const Handles extends readonly [string, ...string[]]>(
143
359
  name: Name,
144
360
  handles: Handles
145
- ): Closed<Name, Handles, Record<never, never>>
361
+ ): Closed<Name, Handles[number], Record<never, never>>
146
362
 
147
363
  /**
148
- * Payload tier: declared columns plus ground axioms, every handle with
149
- * every column exactly once (type-enforced by {@link Axioms}).
364
+ * Payload tier: declared columns AND ground axioms, one call — `closed(
365
+ * "Grade", { mastered: bool }, { DirectPass: { mastered: true }, Failed:
366
+ * { mastered: false } })`. The curried tier-2 spelling is DELETED
367
+ * (canonical utterance): `Cols` infers from the column block, the handle
368
+ * set from the axioms record's keys (reverse mapped-type inference), and
369
+ * every row is contextually checked against the declared columns — a
370
+ * wrong-typed value errors ON its property. The axioms record's keys ARE
371
+ * the handles (declaration order = key order, integer-index names
372
+ * rejected); every row carries every column exactly once (type-enforced by
373
+ * {@link Axioms}).
150
374
  */
151
- function closed<
152
- const Name extends string,
153
- const Handles extends readonly [string, ...string[]],
154
- const Cols extends Record<string, PayloadField>
155
- >(name: Name, handles: Handles, columns: Cols, axioms: Axioms<Handles, Cols>): Closed<Name, Handles, Cols>
375
+ function closed<const Name extends string, const Cols extends PayloadColumns, Handles extends string>(
376
+ name: Name,
377
+ columns: Cols,
378
+ axioms: Axioms<Handles, Cols>
379
+ ): Closed<Name, Handles, Cols>
156
380
 
157
- function closed(
158
- name: string,
159
- handles: readonly [string, ...string[]],
160
- columns?: Record<string, PayloadField>,
161
- axioms?: Readonly<Record<string, Readonly<Record<string, unknown>>>>
162
- ): unknown {
381
+ function closed<const Name extends string, const Cols extends PayloadColumns, Handles extends string>(
382
+ name: Name,
383
+ shape: readonly [string, ...string[]] | Cols,
384
+ axioms?: Axioms<Handles, Cols>
385
+ ): Closed<Name, string, Record<never, never>> | Closed<Name, Handles, Cols> {
386
+ if (isHandleTuple(shape)) {
387
+ if (axioms !== undefined) {
388
+ throw errors.new(`closed relation ${name}: the bare tier declares no columns, so ground axioms are inadmissible`)
389
+ }
390
+ return closedBare(name, shape)
391
+ }
392
+ if (axioms === undefined) {
393
+ throw errors.new(
394
+ `closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, columns, axioms) (the curried spelling is deleted)`
395
+ )
396
+ }
397
+ return closedPayload(name, shape, axioms)
398
+ }
399
+
400
+ /** The bare tier's precisely-typed builder: no columns, no axioms. */
401
+ function closedBare<Name extends string, Handles extends string>(
402
+ name: Name,
403
+ handles: readonly [Handles, ...Handles[]]
404
+ ): Closed<Name, Handles, Record<never, never>> {
405
+ return mintClosed<Name, Handles, Record<never, never>>(name, handles, {}, undefined)
406
+ }
407
+
408
+ /**
409
+ * The payload tier's precisely-typed builder: column names are judged
410
+ * first (the macro-expansion analog), then the handle set is read off the
411
+ * axioms record's own keys.
412
+ */
413
+ function closedPayload<Name extends string, Handles extends string, Cols extends PayloadColumns>(
414
+ name: Name,
415
+ columns: Cols,
416
+ axioms: Axioms<Handles, Cols>
417
+ ): Closed<Name, Handles, Cols> {
418
+ for (const columnName of Object.keys(columns)) {
419
+ assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
420
+ }
421
+ const handles = Object.keys(axioms)
422
+ for (const handle of handles) {
423
+ assertDeclarationOrderKey(`closed relation ${name} handle`, handle)
424
+ }
425
+ if (!handleKeysOwn(axioms, handles)) {
426
+ throw errors.new(`closed relation ${name}: handle enumeration incomplete`)
427
+ }
428
+ return mintClosed<Name, Handles, Cols>(name, handles, columns, axioms)
429
+ }
430
+
431
+ /**
432
+ * The trusted seam of the ergonomic-surface mint: `match` reads back as an
433
+ * own function on BOTH tiers, and `where` exactly when payload columns
434
+ * exist — the runtime twin of the {@link Closed} type's conditional arm
435
+ * (`ClosedMatchBare` vs `ClosedSelectable & ClosedMatchPayload`), verified
436
+ * before the minted value is admitted at the conditional type.
437
+ */
438
+ function surfaceMinted<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
439
+ value: ClosedCore<Name, Handles, Cols> & { readonly [H in Handles]: bigint },
440
+ cols: readonly ClosedColumn[]
441
+ ): value is Closed<Name, Handles, Cols> {
442
+ const matchable = "match" in value && typeof value.match === "function"
443
+ const selectable = "where" in value && typeof value.where === "function"
444
+ return matchable && (cols.length > 0 ? selectable : !selectable)
445
+ }
446
+
447
+ /**
448
+ * Mints one closed relation value — the shared seam of both tiers, HONESTLY
449
+ * typed end to end (a wrong-shaped mint is a compile error here, not a
450
+ * laundered `unknown`): roster checks, eager axiom lowering, the
451
+ * roster-carrying `id` descriptor, the frozen `columns` carrier (the runtime
452
+ * twin of the `Cols` type parameter), the handle constants, the exhaustive
453
+ * `match()` on both tiers, and — on the payload tier only — the
454
+ * ψ-selection `where()`.
455
+ */
456
+ function mintClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
457
+ name: Name,
458
+ handles: readonly Handles[],
459
+ columns: Cols,
460
+ axioms: Axioms<Handles, Cols> | undefined
461
+ ): Closed<Name, Handles, Cols> {
462
+ if (handles.length === 0) {
463
+ throw errors.new(`closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`)
464
+ }
163
465
  const seen = new Set<string>()
164
466
  for (const handle of handles) {
165
467
  if (seen.has(handle)) {
@@ -172,17 +474,22 @@ function closed(
172
474
  )
173
475
  }
174
476
  }
175
- const roster: ClosedRoster = Object.freeze({ name, handles: Object.freeze([...handles]) })
477
+ const handleList: readonly Handles[] = Object.freeze([...handles])
478
+ const roster: ClosedRoster = Object.freeze({ name, handles: handleList })
176
479
  const cols: ClosedColumn[] = []
177
- if (columns !== undefined) {
178
- for (const [columnName, field] of Object.entries(columns)) {
179
- assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
180
- cols.push(Object.freeze({ name: columnName, field: field.data }))
480
+ for (const [columnName, field] of Object.entries(columns)) {
481
+ assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
482
+ if (columnName === "id") {
483
+ throw errors.new(
484
+ `closed relation ${name}: the payload column id collides with the sealed shape's synthetic id (the relation mints its own id at ordinal 0; name the column something else)`
485
+ )
181
486
  }
487
+ cols.push(Object.freeze({ name: columnName, field }))
182
488
  }
183
- const rows: ClosedRow[] = handles.map(function lowerRow(handle) {
489
+ Object.freeze(cols)
490
+ const rows: ClosedRow[] = handleList.map(function lowerRow(handle) {
184
491
  const values = cols.map(function lowerAxiomLiteral(column) {
185
- const row = axiomRow(name, axioms, handle)
492
+ const row = groundRow(name, axioms, handle)
186
493
  return Object.freeze(literalOf(column.field, row[column.name]))
187
494
  })
188
495
  return Object.freeze({ handle, values: Object.freeze(values) })
@@ -190,39 +497,85 @@ function closed(
190
497
  const data: ClosedData = Object.freeze({
191
498
  name,
192
499
  handles: roster.handles,
193
- columns: Object.freeze(cols),
500
+ columns: cols,
194
501
  rows: Object.freeze(rows)
195
502
  })
196
- const id: ClosedIdField<string> = Object.freeze({
197
- data: fieldData({ kind: "u64" }, name, false, roster)
198
- })
503
+ const id: ClosedIdField = Object.freeze({ kind: "u64", closed: roster })
199
504
  /**
200
505
  * Handle names are arbitrary identifiers, so rows and constants are
201
- * minted with OWN-property definition, never assignment: a handle named
506
+ * minted with OWN-property definition (inside {@link mintAxioms} and
507
+ * {@link mintHandleConstants}), never assignment: a handle named
202
508
  * "__proto__" would otherwise ride the Object.prototype accessor —
203
509
  * silently swapping the record's prototype instead of creating the row,
204
510
  * and no-oping the constant (a primitive through the setter) — minting a
205
- * value whose type claims Brand<bigint, Name> but reads back an object.
511
+ * value whose type claims a bigint constant but reads back an object.
512
+ * (Object SPREAD is CreateDataProperty by spec, so the copies below are
513
+ * own-property safe for column names too.)
206
514
  */
207
- const axiomsOut: Record<string, object> = {}
208
- for (const handle of handles) {
209
- const row = axioms === undefined ? Object.freeze({}) : Object.freeze({ ...axiomRow(name, axioms, handle) })
210
- Object.defineProperty(axiomsOut, handle, { value: row, enumerable: true })
515
+ const axiomsOut = mintAxioms<Handles, Cols>(name, handleList, cols, axioms)
516
+ const constants = mintHandleConstants(name, handleList)
517
+ const columnsOut: Cols = { ...columns }
518
+ Object.freeze(columnsOut)
519
+ function fromId(idValue: bigint): Handles | undefined {
520
+ return handleList[Number(idValue)]
211
521
  }
212
- const value: Record<string, unknown> = {
213
- name,
214
- id,
215
- data,
216
- axioms: Object.freeze(axiomsOut),
217
- fromId(idValue: bigint): string | undefined {
218
- return roster.handles[Number(idValue)]
522
+ const holder: { value: Closed<Name, Handles, Cols> | undefined } = { value: undefined }
523
+ /**
524
+ * The ψ selection: resolved against the declared payload columns through
525
+ * the ONE selection machine (`relation.ts::resolveSelection` — a
526
+ * `ClosedColumn` is structurally a `RelationField`), never pre-folded
527
+ * into an id set (the engine folds at validate).
528
+ */
529
+ function where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols> {
530
+ const owner = holder.value
531
+ if (owner === undefined) {
532
+ throw errors.new(`closed relation ${name}: self-reference read before construction completed`)
219
533
  }
534
+ return Object.freeze({
535
+ relation: owner,
536
+ selection: resolveSelection(name, cols, Object.entries(selection))
537
+ })
220
538
  }
221
- handles.forEach(function mintHandleConstant(handle, index) {
222
- Object.defineProperty(value, handle, { value: BigInt(index), enumerable: true })
223
- })
224
- return Object.freeze(value)
539
+ /**
540
+ * The exhaustive dispatch: the ROSTER judges the id (the structural type
541
+ * admits any bigint — a dishonest id is a THROW, never a misdispatch),
542
+ * and the chosen arm receives the handle's frozen axiom row (the bare
543
+ * tier's arms are typed to take nothing; the empty row rides along
544
+ * unread). One implementation serves both tiers — the conditional
545
+ * {@link Closed} arm claims the tier-exact arm signature and
546
+ * {@link surfaceMinted} is the trusted seam that admits it.
547
+ */
548
+ function match<T>(idValue: bigint, arms: { readonly [H in Handles]: (row: AxiomRow<Cols>) => T }): T {
549
+ const handle = fromId(idValue)
550
+ if (handle === undefined) {
551
+ throw errors.new(`closed relation ${name}: match on id ${idValue} misses the roster (${handleList.join(", ")})`)
552
+ }
553
+ return arms[handle](axiomsOut[handle])
554
+ }
555
+ const core = { name, id, data, axioms: axiomsOut, columns: columnsOut, fromId }
556
+ const value: ClosedCore<Name, Handles, Cols> & { readonly [H in Handles]: bigint } =
557
+ cols.length > 0
558
+ ? Object.freeze({ ...constants, ...core, where, match })
559
+ : Object.freeze({ ...constants, ...core, match })
560
+ if (!surfaceMinted<Name, Handles, Cols>(value, cols)) {
561
+ throw errors.new(`closed relation ${name}: ergonomic-surface minting incomplete`)
562
+ }
563
+ holder.value = value
564
+ return value
225
565
  }
226
566
 
227
- export type { AnyClosed, AxiomRow, Axioms, Closed, ClosedColumn, ClosedCore, ClosedData, ClosedRow, PayloadField }
567
+ export type {
568
+ AnyClosed,
569
+ AnySelectedClosed,
570
+ AxiomRow,
571
+ Axioms,
572
+ Closed,
573
+ ClosedColumn,
574
+ ClosedCore,
575
+ ClosedData,
576
+ ClosedRow,
577
+ ClosedSelectionInput,
578
+ PayloadField,
579
+ SelectedClosed
580
+ }
228
581
  export { closed }