@bjornpagen/bumbledb 0.3.0 → 0.5.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 (98) hide show
  1. package/COOKBOOK.md +246 -92
  2. package/README.md +31 -16
  3. package/dist/closed.d.ts +80 -75
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +102 -127
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +37 -7
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +95 -55
  10. package/dist/db.js.map +1 -1
  11. package/dist/exhume.d.ts.map +1 -1
  12. package/dist/exhume.js +1 -14
  13. package/dist/exhume.js.map +1 -1
  14. package/dist/face.d.ts +40 -40
  15. package/dist/face.d.ts.map +1 -1
  16. package/dist/face.js +9 -17
  17. package/dist/face.js.map +1 -1
  18. package/dist/fields.d.ts +46 -15
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +58 -29
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +13 -8
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/law.d.ts +2 -1
  27. package/dist/law.d.ts.map +1 -1
  28. package/dist/law.js +15 -14
  29. package/dist/law.js.map +1 -1
  30. package/dist/lower.d.ts.map +1 -1
  31. package/dist/lower.js +1 -7
  32. package/dist/lower.js.map +1 -1
  33. package/dist/marshal.d.ts +33 -6
  34. package/dist/marshal.d.ts.map +1 -1
  35. package/dist/marshal.js +75 -26
  36. package/dist/marshal.js.map +1 -1
  37. package/dist/native.d.ts +21 -2
  38. package/dist/native.d.ts.map +1 -1
  39. package/dist/native.js +20 -3
  40. package/dist/native.js.map +1 -1
  41. package/dist/order.d.ts +36 -0
  42. package/dist/order.d.ts.map +1 -0
  43. package/dist/order.js +135 -0
  44. package/dist/order.js.map +1 -0
  45. package/dist/query/atom.d.ts +76 -28
  46. package/dist/query/atom.d.ts.map +1 -1
  47. package/dist/query/atom.js +12 -16
  48. package/dist/query/atom.js.map +1 -1
  49. package/dist/query/lower.d.ts +5 -8
  50. package/dist/query/lower.d.ts.map +1 -1
  51. package/dist/query/lower.js +308 -72
  52. package/dist/query/lower.js.map +1 -1
  53. package/dist/query/predicate.d.ts.map +1 -1
  54. package/dist/query/predicate.js +34 -2
  55. package/dist/query/predicate.js.map +1 -1
  56. package/dist/query/run.d.ts +15 -5
  57. package/dist/query/run.d.ts.map +1 -1
  58. package/dist/query/run.js +27 -8
  59. package/dist/query/run.js.map +1 -1
  60. package/dist/query/scope.d.ts +37 -29
  61. package/dist/query/scope.d.ts.map +1 -1
  62. package/dist/query/scope.js +19 -47
  63. package/dist/query/scope.js.map +1 -1
  64. package/dist/relation.d.ts +17 -29
  65. package/dist/relation.d.ts.map +1 -1
  66. package/dist/relation.js +41 -38
  67. package/dist/relation.js.map +1 -1
  68. package/dist/schema.d.ts.map +1 -1
  69. package/dist/schema.js +7 -31
  70. package/dist/schema.js.map +1 -1
  71. package/dist/spec.d.ts +3 -2
  72. package/dist/spec.d.ts.map +1 -1
  73. package/dist/spec.js.map +1 -1
  74. package/dist/statements.d.ts +13 -4
  75. package/dist/statements.d.ts.map +1 -1
  76. package/dist/statements.js +75 -8
  77. package/dist/statements.js.map +1 -1
  78. package/package.json +2 -5
  79. package/src/closed.ts +144 -206
  80. package/src/db.ts +143 -68
  81. package/src/exhume.ts +1 -15
  82. package/src/face.ts +38 -48
  83. package/src/fields.ts +103 -49
  84. package/src/index.ts +11 -10
  85. package/src/law.ts +15 -14
  86. package/src/lower.ts +2 -9
  87. package/src/marshal.ts +82 -31
  88. package/src/native.ts +22 -4
  89. package/src/order.ts +156 -0
  90. package/src/query/atom.ts +70 -35
  91. package/src/query/lower.ts +354 -82
  92. package/src/query/predicate.ts +39 -4
  93. package/src/query/run.ts +27 -9
  94. package/src/query/scope.ts +52 -70
  95. package/src/relation.ts +54 -68
  96. package/src/schema.ts +7 -33
  97. package/src/spec.ts +3 -2
  98. package/src/statements.ts +82 -8
package/src/closed.ts CHANGED
@@ -1,34 +1,36 @@
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
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",
4
+ * tiers, one function. Bare tier: `closed("Kind", ["Checking",
14
5
  * "Savings"])`. Payload tier: `closed("Sev", { pages: bool }, { Critical:
15
6
  * { pages: true }, ... })` — one call, three arguments (the curried tier-2
16
7
  * spelling is DELETED — canonical utterance): the axioms record IS the
17
8
  * handle declaration, every handle carrying every column exactly once
18
- * (type-enforced). No fact type and no insert surface existclosed
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.
9
+ * (type-enforced). At the host surface a handle is its NAME a string
10
+ * literal of the roster's union (the drizzle law: translation, not
11
+ * abstraction; dispatch over a vocabulary is native `switch` narrowing, so
12
+ * no match operator is minted and no handle constants exist the literal
13
+ * `"Checking"` is the ONE spelling). Handles are pure DATA, not properties
14
+ * of the value, so NO handle name is reserved: a vocabulary may legally
15
+ * contain handles named `match`, `where`, or `id` the axioms record and
16
+ * the roster are their own namespaces. The value's whole surface: `name`;
17
+ * `id` the field descriptor carrying the CLOSED LINKAGE (the roster —
18
+ * pure structure, no declared domain: the laws type the columns, and
19
+ * `schema()` names the id's generator class `"Kind.id"`) for other
20
+ * relations' field blocks (`kind: Kind.id`); `data` (the lowering
21
+ * carrier); `axioms` (payload readback, `Kind.axioms`); `columns` (the
22
+ * runtime twin of the `Cols` type parameter, which the face layer's
23
+ * structural wall reads); and — exactly when payload columns exist —
24
+ * `where()`, the ψ-selection surface (`Kind.where({ mastered: true })` as a
25
+ * face source), resolved through the ONE selection machine
26
+ * (`relation.ts::resolveSelection`); the bare tier has no payload columns
27
+ * to select on, so `.where` is absent there, at the type AND on the value.
28
+ * No fact type and no insert surface exist — closed relations are
29
+ * unwritable by construction: the value simply lacks the writable relation
30
+ * shape.
28
31
  */
29
32
 
30
33
  import * as errors from "@superbuilders/errors"
31
- import type { OneOf } from "#face.ts"
32
34
  import {
33
35
  type AnyField,
34
36
  assertDeclarationOrderKey,
@@ -37,31 +39,10 @@ import {
37
39
  type Infer,
38
40
  literalOf
39
41
  } from "#fields.ts"
40
- import { resolveSelection, type SelectionBinding } from "#relation.ts"
42
+ import type { AnyRelation, RelationField } from "#relation.ts"
43
+ import { resolveSelection, type SelectionBinding, type SelectionInput } from "#relation.ts"
41
44
  import type { LiteralSpec } from "#spec.ts"
42
45
 
43
- /**
44
- * The value-surface property names a handle may not shadow — the macro's
45
- * name-collision diagnostic, here over the closed value's own properties
46
- * (`relation`/`selection` are reserved so a closed value can never be
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).
51
- */
52
- const reservedHandleNames: readonly string[] = Object.freeze([
53
- "name",
54
- "id",
55
- "data",
56
- "axioms",
57
- "columns",
58
- "fromId",
59
- "where",
60
- "match",
61
- "relation",
62
- "selection"
63
- ])
64
-
65
46
  /**
66
47
  * A payload column of a closed relation: any field descriptor except a
67
48
  * fresh-marked one (a vocabulary's rows are ground axioms, never minted).
@@ -73,7 +54,7 @@ type PayloadField = Exclude<AnyField, { readonly fresh: true }>
73
54
  * unspellable — the sealed shape mints the synthetic `id` itself (ordinal
74
55
  * 0 of the matchable fields), so a declared column named `id` would be
75
56
  * shadowed by the synthetic slot everywhere the shape resolves by name
76
- * (`matchFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
57
+ * (`sealedFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
77
58
  * typed here and judged again at construction in {@link mintClosed} — the
78
59
  * runtime twin for untyped callers, warmer and earlier than the engine's
79
60
  * `DuplicateFieldName` at `Db.create`.
@@ -126,12 +107,14 @@ interface ClosedCore<Name extends string, Handles extends string, Cols extends R
126
107
  readonly name: Name
127
108
  /**
128
109
  * 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).
110
+ * field block is the reference through which handle literals become
111
+ * legal in that relation's selections. Pure structure plus the PRECISE
112
+ * roster (`ClosedIdField<Handles>` the handle union is the field's
113
+ * value type under `Infer`); the referencing field's domain is law-born:
114
+ * `schema()` computes it from the declared containment (`"Kind.id"`, the
115
+ * generator class).
133
116
  */
134
- readonly id: ClosedIdField
117
+ readonly id: ClosedIdField<Handles>
135
118
  readonly data: ClosedData
136
119
  /** Payload readback: handle to its declared column values, bare and structural. */
137
120
  readonly axioms: Axioms<Handles, Cols>
@@ -144,20 +127,18 @@ interface ClosedCore<Name extends string, Handles extends string, Cols extends R
144
127
  * descriptors in declaration order for the lowering).
145
128
  */
146
129
  readonly columns: Cols
147
- /** The weld: declaration-order id back to its handle, or undefined beyond the roster. */
148
- fromId(id: bigint): Handles | undefined
149
130
  }
150
131
 
151
132
  /**
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).
133
+ * The `where()` argument of a closed relation: EXACTLY the relation
134
+ * surface's {@link SelectionInput}, over the declared payload columns the
135
+ * ONE selection vocabulary, so a spelling change there (H3's membership
136
+ * arrays) flows through with no local change here. The synthetic `id` is
137
+ * deliberately unspellable ({@link PayloadColumns} refuses an `id` column):
138
+ * an id selection is spelled only as handle literals on the REFERENCING
139
+ * side (the canonical-utterance law).
157
140
  */
158
- type ClosedSelectionInput<Cols extends Record<string, PayloadField>> = {
159
- readonly [C in keyof Cols]?: Infer<Cols[C]> | OneOf<Infer<Cols[C]>>
160
- }
141
+ type ClosedSelectionInput<Cols extends Record<string, PayloadField>> = SelectionInput<Cols>
161
142
 
162
143
  /**
163
144
  * A closed relation with a ψ selection applied — what `on()` consumes as a
@@ -190,52 +171,69 @@ interface ClosedSelectable<Name extends string, Handles extends string, Cols ext
190
171
  }
191
172
 
192
173
  /**
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` inthe runtime roster refuses an out-of-vocabulary id
199
- * with a throw, never a misdispatch.
174
+ * A closed relation value: the core surface plus exactly when payload
175
+ * columns exist `where()` (the bare tier has nothing to select on, so
176
+ * the method is ABSENT there, not merely uncallable). NOTHING else:
177
+ * handles are data on the roster, never properties of the value (the
178
+ * handle constants, the match operator, and the id-to-handle weld died
179
+ * with the bigint eradispatch is native `switch` narrowing over the
180
+ * handle union).
200
181
  */
201
- interface ClosedMatchBare<Handles extends string> {
202
- match<T>(id: bigint, arms: { readonly [H in Handles]: () => T }): T
182
+ type Closed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> = [
183
+ keyof Cols
184
+ ] extends [never]
185
+ ? ClosedCore<Name, Handles, Cols>
186
+ : ClosedCore<Name, Handles, Cols> & ClosedSelectable<Name, Handles, Cols>
187
+
188
+ /** Any closed relation value, whatever its roster and columns. */
189
+ interface AnyClosed {
190
+ readonly name: string
191
+ readonly id: ClosedIdField
192
+ readonly data: ClosedData
193
+ readonly axioms: Readonly<Record<string, object>>
194
+ readonly columns: Readonly<Record<string, PayloadField>>
203
195
  }
204
196
 
205
197
  /**
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`).
198
+ * THE relation-kind discriminant the ONE spelling of "is this schema
199
+ * member closed?" (the type tier's twin is the `AnyClosed` conditional
200
+ * arms). A closed relation's runtime description carries its handle
201
+ * roster; an ordinary relation's never does. Every runtime closed/ordinary
202
+ * fork in the SDK judges through this predicate — never a re-spelled
203
+ * structural probe.
210
204
  */
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
205
+ function isClosedMember(member: AnyRelation | AnyClosed): member is AnyClosed {
206
+ return "handles" in member.data
213
207
  }
214
208
 
215
209
  /**
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).
210
+ * The SEALED field list of a schema member — THE one reader of "what
211
+ * fields does this owner expose": an ordinary relation's declared fields;
212
+ * a closed relation's sealed shape the synthetic `id` (the value's own
213
+ * roster-carrying descriptor, by identity) at ordinal 0, then the declared
214
+ * payload columns at declared index + 1 (the sealed shift, mirroring the
215
+ * engine's `SchemaDescriptor::sealed_fields`). A `ClosedColumn` is
216
+ * structurally a `RelationField`, so both kinds read uniformly.
223
217
  */
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>)
218
+ function sealedFieldsOf(member: AnyRelation | AnyClosed): readonly RelationField[] {
219
+ if (isClosedMember(member)) {
220
+ return Object.freeze([Object.freeze({ name: "id", field: member.id }), ...member.data.columns])
221
+ }
222
+ return member.data.fields
223
+ }
231
224
 
232
- /** Any closed relation value, whatever its roster and columns. */
233
- interface AnyClosed {
234
- readonly name: string
235
- readonly id: ClosedIdField
236
- readonly data: ClosedData
237
- readonly axioms: Readonly<Record<string, object>>
238
- readonly columns: Readonly<Record<string, PayloadField>>
225
+ /**
226
+ * One sealed field by name — derived from {@link sealedFieldsOf}, so the
227
+ * closed synthetic `id` resolves everywhere a name is looked up (no reader
228
+ * can silently lack the `id` arm). `undefined` when the name is foreign
229
+ * (the type tiers make that unwritable; the engine re-judges at
230
+ * `Db.create`).
231
+ */
232
+ function sealedFieldOf(member: AnyRelation | AnyClosed, fieldName: string): AnyField | undefined {
233
+ const declared = sealedFieldsOf(member).find(function byName(candidate) {
234
+ return candidate.name === fieldName
235
+ })
236
+ return declared?.field
239
237
  }
240
238
 
241
239
  /** Narrows the two-tier second argument: a handle tuple (bare tier) or a column block (payload tier). */
@@ -264,7 +262,8 @@ function handleKeysOwn<Handles extends string>(
264
262
  * The trusted seam of the axiom-readback mint: every handle carries an own
265
263
  * frozen row and every row carries every declared column as an own
266
264
  * property — verified before the record is admitted as the typed
267
- * {@link Axioms} (the `refsComplete` analog of `relation()`).
265
+ * {@link Axioms} (the trusted-admission-seam pattern its home is
266
+ * `isTypedScope` in query/lower.ts).
268
267
  */
269
268
  function axiomsMinted<Handles extends string, Cols extends Record<string, PayloadField>>(
270
269
  record: Readonly<Record<string, object>>,
@@ -282,53 +281,22 @@ function axiomsMinted<Handles extends string, Cols extends Record<string, Payloa
282
281
  })
283
282
  }
284
283
 
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
284
  /**
319
285
  * Mints the axiom-readback record: one own frozen row per handle (the bare
320
286
  * tier's rows are empty — it declares no columns), each row a fresh copy of
321
- * its ground axiom.
287
+ * its ground axiom. Both tiers supply a REAL axioms record ({@link
288
+ * closedBare} mints its empty rows), so the columns-without-axioms state is
289
+ * unrepresentable here — no undefined arm exists to guard.
322
290
  */
323
291
  function mintAxioms<Handles extends string, Cols extends Record<string, PayloadField>>(
324
292
  name: string,
325
293
  handles: readonly Handles[],
326
294
  cols: readonly ClosedColumn[],
327
- axioms: Axioms<Handles, Cols> | undefined
295
+ axioms: Axioms<Handles, Cols>
328
296
  ): Axioms<Handles, Cols> {
329
297
  const out: Record<string, object> = {}
330
298
  for (const handle of handles) {
331
- const row = axioms === undefined ? Object.freeze({}) : Object.freeze({ ...groundRow(name, axioms, handle) })
299
+ const row = Object.freeze({ ...axioms[handle] })
332
300
  Object.defineProperty(out, handle, { value: row, enumerable: true })
333
301
  }
334
302
  Object.freeze(out)
@@ -338,22 +306,6 @@ function mintAxioms<Handles extends string, Cols extends Record<string, PayloadF
338
306
  return out
339
307
  }
340
308
 
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
355
- }
356
-
357
309
  /** Bare tier: `closed("Kind", ["Checking", "Savings"])` — handles only. */
358
310
  function closed<const Name extends string, const Handles extends readonly [string, ...string[]]>(
359
311
  name: Name,
@@ -397,12 +349,29 @@ function closed<const Name extends string, const Cols extends PayloadColumns, Ha
397
349
  return closedPayload(name, shape, axioms)
398
350
  }
399
351
 
400
- /** The bare tier's precisely-typed builder: no columns, no axioms. */
352
+ /**
353
+ * The bare tier's precisely-typed builder: no columns, and the axioms
354
+ * record is the EMPTY-ROW record over the handle roster (one own frozen
355
+ * `{}` per handle, `__proto__`-safe own-property definition) — the same
356
+ * representation the payload tier carries, so `mintClosed` never sees a
357
+ * tier fork and the columns-without-axioms state stops being spellable.
358
+ */
401
359
  function closedBare<Name extends string, Handles extends string>(
402
360
  name: Name,
403
361
  handles: readonly [Handles, ...Handles[]]
404
362
  ): Closed<Name, Handles, Record<never, never>> {
405
- return mintClosed<Name, Handles, Record<never, never>>(name, handles, {}, undefined)
363
+ const empty: Record<string, object> = {}
364
+ for (const handle of handles) {
365
+ /** A duplicated name mints one row; the roster's own duplicate refusal in {@link mintClosed} stays the judge. */
366
+ if (!Object.hasOwn(empty, handle)) {
367
+ Object.defineProperty(empty, handle, { value: Object.freeze({}), enumerable: true })
368
+ }
369
+ }
370
+ Object.freeze(empty)
371
+ if (!axiomsMinted<Handles, Record<never, never>>(empty, handles, [])) {
372
+ throw errors.new(`closed relation ${name}: bare-tier axiom-row minting incomplete`)
373
+ }
374
+ return mintClosed<Name, Handles, Record<never, never>>(name, handles, {}, empty)
406
375
  }
407
376
 
408
377
  /**
@@ -429,19 +398,18 @@ function closedPayload<Name extends string, Handles extends string, Cols extends
429
398
  }
430
399
 
431
400
  /**
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.
401
+ * The trusted seam of the ergonomic-surface mint: `where` reads back as an
402
+ * own function exactly when payload columns exist, and is ABSENT otherwise
403
+ * — the runtime twin of the {@link Closed} type's conditional arm
404
+ * (`ClosedCore` alone vs `ClosedCore & ClosedSelectable`), verified before
405
+ * the minted value is admitted at the conditional type.
437
406
  */
438
407
  function surfaceMinted<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
439
- value: ClosedCore<Name, Handles, Cols> & { readonly [H in Handles]: bigint },
408
+ value: ClosedCore<Name, Handles, Cols>,
440
409
  cols: readonly ClosedColumn[]
441
- ): value is Closed<Name, Handles, Cols> {
442
- const matchable = "match" in value && typeof value.match === "function"
410
+ ): value is ClosedCore<Name, Handles, Cols> & Closed<Name, Handles, Cols> {
443
411
  const selectable = "where" in value && typeof value.where === "function"
444
- return matchable && (cols.length > 0 ? selectable : !selectable)
412
+ return cols.length > 0 ? selectable : !selectable
445
413
  }
446
414
 
447
415
  /**
@@ -449,15 +417,14 @@ function surfaceMinted<Name extends string, Handles extends string, Cols extends
449
417
  * typed end to end (a wrong-shaped mint is a compile error here, not a
450
418
  * laundered `unknown`): roster checks, eager axiom lowering, the
451
419
  * 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
420
+ * twin of the `Cols` type parameter), and — on the payload tier only — the
454
421
  * ψ-selection `where()`.
455
422
  */
456
423
  function mintClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
457
424
  name: Name,
458
425
  handles: readonly Handles[],
459
426
  columns: Cols,
460
- axioms: Axioms<Handles, Cols> | undefined
427
+ axioms: Axioms<Handles, Cols>
461
428
  ): Closed<Name, Handles, Cols> {
462
429
  if (handles.length === 0) {
463
430
  throw errors.new(`closed relation ${name}: at least one handle is required (an empty vocabulary declares nothing)`)
@@ -468,14 +435,9 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
468
435
  throw errors.new(`closed relation ${name}: duplicate handle ${handle}`)
469
436
  }
470
437
  seen.add(handle)
471
- if (reservedHandleNames.includes(handle)) {
472
- throw errors.new(
473
- `closed relation ${name}: handle ${handle} collides with the closed value's own surface (${reservedHandleNames.join(", ")})`
474
- )
475
- }
476
438
  }
477
439
  const handleList: readonly Handles[] = Object.freeze([...handles])
478
- const roster: ClosedRoster = Object.freeze({ name, handles: handleList })
440
+ const roster: ClosedRoster<Handles> = Object.freeze({ name, handles: handleList })
479
441
  const cols: ClosedColumn[] = []
480
442
  for (const [columnName, field] of Object.entries(columns)) {
481
443
  assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
@@ -488,8 +450,8 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
488
450
  }
489
451
  Object.freeze(cols)
490
452
  const rows: ClosedRow[] = handleList.map(function lowerRow(handle) {
453
+ const row: Readonly<Record<string, unknown>> = axioms[handle]
491
454
  const values = cols.map(function lowerAxiomLiteral(column) {
492
- const row = groundRow(name, axioms, handle)
493
455
  return Object.freeze(literalOf(column.field, row[column.name]))
494
456
  })
495
457
  return Object.freeze({ handle, values: Object.freeze(values) })
@@ -500,25 +462,18 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
500
462
  columns: cols,
501
463
  rows: Object.freeze(rows)
502
464
  })
503
- const id: ClosedIdField = Object.freeze({ kind: "u64", closed: roster })
465
+ const id: ClosedIdField<Handles> = Object.freeze({ kind: "u64", closed: roster })
504
466
  /**
505
- * Handle names are arbitrary identifiers, so rows and constants are
506
- * minted with OWN-property definition (inside {@link mintAxioms} and
507
- * {@link mintHandleConstants}), never assignment: a handle named
508
- * "__proto__" would otherwise ride the Object.prototype accessor
509
- * silently swapping the record's prototype instead of creating the row,
510
- * and no-oping the constant (a primitive through the setter) — minting a
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.)
467
+ * Handle names are arbitrary identifiers, so axiom rows are minted with
468
+ * OWN-property definition (inside {@link mintAxioms}), never assignment:
469
+ * a handle named "__proto__" would otherwise ride the Object.prototype
470
+ * accessor silently swapping the record's prototype instead of
471
+ * creating the row. (Object SPREAD is CreateDataProperty by spec, so the
472
+ * copies below are own-property safe for column names too.)
514
473
  */
515
474
  const axiomsOut = mintAxioms<Handles, Cols>(name, handleList, cols, axioms)
516
- const constants = mintHandleConstants(name, handleList)
517
475
  const columnsOut: Cols = { ...columns }
518
476
  Object.freeze(columnsOut)
519
- function fromId(idValue: bigint): Handles | undefined {
520
- return handleList[Number(idValue)]
521
- }
522
477
  const holder: { value: Closed<Name, Handles, Cols> | undefined } = { value: undefined }
523
478
  /**
524
479
  * The ψ selection: resolved against the declared payload columns through
@@ -536,27 +491,9 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
536
491
  selection: resolveSelection(name, cols, Object.entries(selection))
537
492
  })
538
493
  }
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 })
494
+ const core = { name, id, data, axioms: axiomsOut, columns: columnsOut }
495
+ const value: ClosedCore<Name, Handles, Cols> =
496
+ cols.length > 0 ? Object.freeze({ ...core, where }) : Object.freeze(core)
560
497
  if (!surfaceMinted<Name, Handles, Cols>(value, cols)) {
561
498
  throw errors.new(`closed relation ${name}: ergonomic-surface minting incomplete`)
562
499
  }
@@ -574,8 +511,9 @@ export type {
574
511
  ClosedCore,
575
512
  ClosedData,
576
513
  ClosedRow,
514
+ ClosedSelectable,
577
515
  ClosedSelectionInput,
578
516
  PayloadField,
579
517
  SelectedClosed
580
518
  }
581
- export { closed }
519
+ export { closed, isClosedMember, sealedFieldOf, sealedFieldsOf }