@bjornpagen/bumbledb 0.2.0 → 0.4.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 (94) hide show
  1. package/COOKBOOK.md +503 -427
  2. package/README.md +84 -36
  3. package/dist/closed.d.ts +111 -38
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +94 -99
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +16 -2
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +46 -8
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +114 -69
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js +38 -21
  14. package/dist/face.js.map +1 -1
  15. package/dist/fields.d.ts +72 -87
  16. package/dist/fields.d.ts.map +1 -1
  17. package/dist/fields.js +35 -67
  18. package/dist/fields.js.map +1 -1
  19. package/dist/index.d.ts +18 -13
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +11 -7
  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/marshal.d.ts +33 -6
  32. package/dist/marshal.d.ts.map +1 -1
  33. package/dist/marshal.js +67 -6
  34. package/dist/marshal.js.map +1 -1
  35. package/dist/native.d.ts +6 -2
  36. package/dist/native.d.ts.map +1 -1
  37. package/dist/native.js.map +1 -1
  38. package/dist/query/atom.d.ts +139 -56
  39. package/dist/query/atom.d.ts.map +1 -1
  40. package/dist/query/atom.js +7 -1
  41. package/dist/query/atom.js.map +1 -1
  42. package/dist/query/lower.d.ts +71 -56
  43. package/dist/query/lower.d.ts.map +1 -1
  44. package/dist/query/lower.js +341 -69
  45. package/dist/query/lower.js.map +1 -1
  46. package/dist/query/predicate.d.ts +10 -9
  47. package/dist/query/predicate.d.ts.map +1 -1
  48. package/dist/query/predicate.js +2 -2
  49. package/dist/query/predicate.js.map +1 -1
  50. package/dist/query/run.d.ts +15 -5
  51. package/dist/query/run.d.ts.map +1 -1
  52. package/dist/query/run.js +26 -6
  53. package/dist/query/run.js.map +1 -1
  54. package/dist/query/scope.d.ts +100 -43
  55. package/dist/query/scope.d.ts.map +1 -1
  56. package/dist/query/scope.js +89 -30
  57. package/dist/query/scope.js.map +1 -1
  58. package/dist/query/select.d.ts +5 -5
  59. package/dist/query/select.d.ts.map +1 -1
  60. package/dist/relation.d.ts +29 -15
  61. package/dist/relation.d.ts.map +1 -1
  62. package/dist/relation.js +45 -17
  63. package/dist/relation.js.map +1 -1
  64. package/dist/schema.d.ts +41 -3
  65. package/dist/schema.d.ts.map +1 -1
  66. package/dist/schema.js +16 -2
  67. package/dist/schema.js.map +1 -1
  68. package/dist/spec.d.ts +10 -8
  69. package/dist/spec.d.ts.map +1 -1
  70. package/dist/spec.js.map +1 -1
  71. package/dist/statements.d.ts +67 -31
  72. package/dist/statements.d.ts.map +1 -1
  73. package/dist/statements.js +97 -20
  74. package/dist/statements.js.map +1 -1
  75. package/package.json +2 -2
  76. package/src/closed.ts +214 -146
  77. package/src/db.ts +65 -10
  78. package/src/face.ts +169 -102
  79. package/src/fields.ts +97 -164
  80. package/src/index.ts +43 -18
  81. package/src/law.ts +519 -0
  82. package/src/lower.ts +36 -23
  83. package/src/marshal.ts +74 -7
  84. package/src/native.ts +6 -2
  85. package/src/query/atom.ts +155 -65
  86. package/src/query/lower.ts +572 -167
  87. package/src/query/predicate.ts +43 -33
  88. package/src/query/run.ts +26 -6
  89. package/src/query/scope.ts +161 -51
  90. package/src/query/select.ts +5 -5
  91. package/src/relation.ts +60 -26
  92. package/src/schema.ts +48 -7
  93. package/src/spec.ts +10 -8
  94. package/src/statements.ts +165 -46
package/src/closed.ts CHANGED
@@ -1,20 +1,33 @@
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 handle
8
- * DOMAIN (`"KindId"`, mirroring Rust's `closed relation Kind as KindId`)
9
- * for other relations' field blocks (`kind: Kind.id`), payload readback
10
- * (`Kind.axioms`), and the declared payload column descriptors
11
- * (`Kind.columns` the runtime twin of the `Cols` type parameter, which
12
- * the face layer's domain wall reads). Bare tier: `closed("Kind", ["Checking",
13
- * "Savings"])`. Payload tier: `closed("Sev", { pages: bool })({ Critical:
14
- * { pages: true }, ... })` the axioms record IS the handle declaration,
15
- * every handle carrying every column exactly once (type-enforced). No fact
16
- * type and no insert surface exist closed relations are unwritable by
17
- * construction: the value simply lacks the writable relation shape.
4
+ * tiers, one function. Bare tier: `closed("Kind", ["Checking",
5
+ * "Savings"])`. Payload tier: `closed("Sev", { pages: bool }, { Critical:
6
+ * { pages: true }, ... })` one call, three arguments (the curried tier-2
7
+ * spelling is DELETED canonical utterance): the axioms record IS the
8
+ * handle declaration, every handle carrying every column exactly once
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.
18
31
  */
19
32
 
20
33
  import * as errors from "@superbuilders/errors"
@@ -26,31 +39,27 @@ import {
26
39
  type Infer,
27
40
  literalOf
28
41
  } from "#fields.ts"
42
+ import { resolveSelection, type SelectionBinding, type SelectionInput } from "#relation.ts"
29
43
  import type { LiteralSpec } from "#spec.ts"
30
44
 
31
- /**
32
- * The value-surface property names a handle may not shadow — the macro's
33
- * name-collision diagnostic, here over the closed value's own properties
34
- * (`relation`/`selection` are reserved so a closed value can never be
35
- * mistaken for a selected relation by `on()`'s discriminant).
36
- */
37
- const reservedHandleNames: readonly string[] = Object.freeze([
38
- "name",
39
- "id",
40
- "data",
41
- "axioms",
42
- "columns",
43
- "fromId",
44
- "relation",
45
- "selection"
46
- ])
47
-
48
45
  /**
49
46
  * A payload column of a closed relation: any field descriptor except a
50
47
  * fresh-marked one (a vocabulary's rows are ground axioms, never minted).
51
48
  */
52
49
  type PayloadField = Exclude<AnyField, { readonly fresh: true }>
53
50
 
51
+ /**
52
+ * A declared payload column BLOCK: name → descriptor, with `id`
53
+ * unspellable — the sealed shape mints the synthetic `id` itself (ordinal
54
+ * 0 of the matchable fields), so a declared column named `id` would be
55
+ * shadowed by the synthetic slot everywhere the shape resolves by name
56
+ * (`matchFieldsOf`, the projected face, `spec.rs`'s resolver). The wall is
57
+ * typed here and judged again at construction in {@link mintClosed} — the
58
+ * runtime twin for untyped callers, warmer and earlier than the engine's
59
+ * `DuplicateFieldName` at `Db.create`.
60
+ */
61
+ type PayloadColumns = Record<string, PayloadField> & { readonly id?: never }
62
+
54
63
  /** One declared payload column: name plus its field descriptor. */
55
64
  interface ClosedColumn {
56
65
  readonly name: string
@@ -96,39 +105,84 @@ type Axioms<Handles extends string, Cols extends Record<string, PayloadField>> =
96
105
  interface ClosedCore<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
97
106
  readonly name: Name
98
107
  /**
99
- * The handle-domain reference descriptor: `kind: Kind.id` in another
100
- * relation's field block is the reference through which bare handle ids
101
- * become legal in that relation's selections. Its domain is the closed
102
- * relation's handle domain (`"KindId"`Rust's `as KindId`).
108
+ * The closed reference descriptor: `kind: Kind.id` in another relation's
109
+ * field block is the reference through which handle literals become
110
+ * legal in that relation's selections. Pure structure plus the PRECISE
111
+ * roster (`ClosedIdField<Handles>`the handle union is the field's
112
+ * value type under `Infer`); the referencing field's domain is law-born:
113
+ * `schema()` computes it from the declared containment (`"Kind.id"`, the
114
+ * generator class).
103
115
  */
104
- readonly id: ClosedIdField<`${Name}Id`>
116
+ readonly id: ClosedIdField<Handles>
105
117
  readonly data: ClosedData
106
118
  /** Payload readback: handle to its declared column values, bare and structural. */
107
119
  readonly axioms: Axioms<Handles, Cols>
108
120
  /**
109
- * The declared payload columns, name → S1 field descriptor — an HONEST
121
+ * The declared payload columns, name → field descriptor — an HONEST
110
122
  * frozen runtime record (the descriptors themselves, by identity), and
111
- * the typed carrier a projected payload column's domain label is
123
+ * the typed carrier a projected payload column's structural shape is
112
124
  * recovered through off the schema type (the face layer's
113
- * `ProjectedDomain` reads it; `data.columns` carries the same
125
+ * `ProjectedShape` reads it; `data.columns` carries the same
114
126
  * descriptors in declaration order for the lowering).
115
127
  */
116
128
  readonly columns: Cols
117
- /** The weld: declaration-order id back to its handle, or undefined beyond the roster. */
118
- fromId(id: bigint): Handles | undefined
119
129
  }
120
130
 
121
131
  /**
122
- * A closed relation value: the core surface plus one BARE constant per
123
- * handle (`Kind.Checking: bigint`, ids = declaration order — the value is
124
- * structural; the roster judges out-of-vocabulary ids at construction and
125
- * the engine at commit).
132
+ * The `where()` argument of a closed relation: EXACTLY the relation
133
+ * surface's {@link SelectionInput}, over the declared payload columns — the
134
+ * ONE selection vocabulary, so a spelling change there (H3's membership
135
+ * arrays) flows through with no local change here. The synthetic `id` is
136
+ * deliberately unspellable ({@link PayloadColumns} refuses an `id` column):
137
+ * an id selection is spelled only as handle literals on the REFERENCING
138
+ * side (the canonical-utterance law).
139
+ */
140
+ type ClosedSelectionInput<Cols extends Record<string, PayloadField>> = SelectionInput<Cols>
141
+
142
+ /**
143
+ * A closed relation with a ψ selection applied — what `on()` consumes as a
144
+ * σ-carrying closed source (`on(Kind.where({ mastered: true }), "id")`).
145
+ * Deliberately the SAME discriminant shape as the ordinary `Selected`
146
+ * (`relation`/`selection` — `face.ts::faceParts` splits both by `"relation"
147
+ * in source`), and structurally UNMISTAKABLE for one: an `AnyClosed` lacks
148
+ * the relation shape (no `fields` record, no `RelationData`).
149
+ */
150
+ interface SelectedClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
151
+ readonly relation: Closed<Name, Handles, Cols>
152
+ readonly selection: readonly SelectionBinding[]
153
+ }
154
+
155
+ /** Any ψ-selected closed relation value. */
156
+ interface AnySelectedClosed {
157
+ readonly relation: AnyClosed
158
+ readonly selection: readonly SelectionBinding[]
159
+ }
160
+
161
+ /**
162
+ * The ψ-selection surface of a payload-tier closed value. The selection is
163
+ * resolved EAGERLY against the declared columns and lowered as-is — the SDK
164
+ * never pre-folds ψ into an id set: pass-through lowering is what the macro
165
+ * does, and the ENGINE folds against the sealed extension at validate
166
+ * (`compile_member_set`).
167
+ */
168
+ interface ClosedSelectable<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> {
169
+ where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols>
170
+ }
171
+
172
+ /**
173
+ * A closed relation value: the core surface plus — exactly when payload
174
+ * columns exist — `where()` (the bare tier has nothing to select on, so
175
+ * the method is ABSENT there, not merely uncallable). NOTHING else:
176
+ * handles are data on the roster, never properties of the value (the
177
+ * handle constants, the match operator, and the id-to-handle weld died
178
+ * with the bigint era — dispatch is native `switch` narrowing over the
179
+ * handle union).
126
180
  */
127
- type Closed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> = ClosedCore<
128
- Name,
129
- Handles,
130
- Cols
131
- > & { readonly [H in Handles]: bigint }
181
+ type Closed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>> = [
182
+ keyof Cols
183
+ ] extends [never]
184
+ ? ClosedCore<Name, Handles, Cols>
185
+ : ClosedCore<Name, Handles, Cols> & ClosedSelectable<Name, Handles, Cols>
132
186
 
133
187
  /** Any closed relation value, whatever its roster and columns. */
134
188
  interface AnyClosed {
@@ -183,21 +237,6 @@ function axiomsMinted<Handles extends string, Cols extends Record<string, Payloa
183
237
  })
184
238
  }
185
239
 
186
- /**
187
- * The trusted seam of the handle-constant mint: every handle reads back as
188
- * an own bigint — verified before the record is admitted at the constants
189
- * type (a "__proto__"-named handle riding the object-protocol accessor
190
- * would fail exactly this check).
191
- */
192
- function constantsMinted<Handles extends string>(
193
- record: Readonly<Record<string, bigint>>,
194
- handles: readonly Handles[]
195
- ): record is Readonly<Record<string, bigint>> & { readonly [H in Handles]: bigint } {
196
- return handles.every(function constantMinted(handle) {
197
- return typeof record[handle] === "bigint"
198
- })
199
- }
200
-
201
240
  /**
202
241
  * Reads one handle's ground axiom row for lowering. The typed payload
203
242
  * surface makes absence unrepresentable ({@link Axioms} carries every
@@ -239,22 +278,6 @@ function mintAxioms<Handles extends string, Cols extends Record<string, PayloadF
239
278
  return out
240
279
  }
241
280
 
242
- /** Mints the handle constants: one own bigint per handle, ids = declaration order. */
243
- function mintHandleConstants<Handles extends string>(
244
- name: string,
245
- handles: readonly Handles[]
246
- ): { readonly [H in Handles]: bigint } {
247
- const out: Record<string, bigint> = {}
248
- handles.forEach(function mintHandleConstant(handle, index) {
249
- Object.defineProperty(out, handle, { value: BigInt(index), enumerable: true })
250
- })
251
- Object.freeze(out)
252
- if (!constantsMinted(out, handles)) {
253
- throw errors.new(`closed relation ${name}: handle-constant minting incomplete`)
254
- }
255
- return out
256
- }
257
-
258
281
  /** Bare tier: `closed("Kind", ["Checking", "Savings"])` — handles only. */
259
282
  function closed<const Name extends string, const Handles extends readonly [string, ...string[]]>(
260
283
  name: Name,
@@ -262,29 +285,40 @@ function closed<const Name extends string, const Handles extends readonly [strin
262
285
  ): Closed<Name, Handles[number], Record<never, never>>
263
286
 
264
287
  /**
265
- * Payload tier: declared columns, then ground axioms — `closed("Grade",
266
- * { mastered: bool })({ DirectPass: { mastered: true }, Failed: { mastered:
267
- * false } })`. The axioms record's keys ARE the handles (declaration order
268
- * = key order, integer-index names rejected); every row carries every
269
- * column exactly once (type-enforced by {@link Axioms}).
288
+ * Payload tier: declared columns AND ground axioms, one call — `closed(
289
+ * "Grade", { mastered: bool }, { DirectPass: { mastered: true }, Failed:
290
+ * { mastered: false } })`. The curried tier-2 spelling is DELETED
291
+ * (canonical utterance): `Cols` infers from the column block, the handle
292
+ * set from the axioms record's keys (reverse mapped-type inference), and
293
+ * every row is contextually checked against the declared columns — a
294
+ * wrong-typed value errors ON its property. The axioms record's keys ARE
295
+ * the handles (declaration order = key order, integer-index names
296
+ * rejected); every row carries every column exactly once (type-enforced by
297
+ * {@link Axioms}).
270
298
  */
271
- function closed<const Name extends string, const Cols extends Record<string, PayloadField>>(
299
+ function closed<const Name extends string, const Cols extends PayloadColumns, Handles extends string>(
272
300
  name: Name,
273
- columns: Cols
274
- ): <Handles extends string>(axioms: Axioms<Handles, Cols>) => Closed<Name, Handles, Cols>
301
+ columns: Cols,
302
+ axioms: Axioms<Handles, Cols>
303
+ ): Closed<Name, Handles, Cols>
275
304
 
276
- function closed<const Name extends string>(
305
+ function closed<const Name extends string, const Cols extends PayloadColumns, Handles extends string>(
277
306
  name: Name,
278
- shape: readonly [string, ...string[]] | Record<string, PayloadField>
279
- ):
280
- | Closed<Name, string, Record<never, never>>
281
- | (<Handles extends string>(
282
- axioms: Axioms<Handles, Record<string, PayloadField>>
283
- ) => Closed<Name, Handles, Record<string, PayloadField>>) {
307
+ shape: readonly [string, ...string[]] | Cols,
308
+ axioms?: Axioms<Handles, Cols>
309
+ ): Closed<Name, string, Record<never, never>> | Closed<Name, Handles, Cols> {
284
310
  if (isHandleTuple(shape)) {
311
+ if (axioms !== undefined) {
312
+ throw errors.new(`closed relation ${name}: the bare tier declares no columns, so ground axioms are inadmissible`)
313
+ }
285
314
  return closedBare(name, shape)
286
315
  }
287
- return closedPayload(name, shape)
316
+ if (axioms === undefined) {
317
+ throw errors.new(
318
+ `closed relation ${name}: payload columns declared without ground axioms — the payload tier is spelled closed(name, columns, axioms) (the curried spelling is deleted)`
319
+ )
320
+ }
321
+ return closedPayload(name, shape, axioms)
288
322
  }
289
323
 
290
324
  /** The bare tier's precisely-typed builder: no columns, no axioms. */
@@ -297,35 +331,49 @@ function closedBare<Name extends string, Handles extends string>(
297
331
 
298
332
  /**
299
333
  * The payload tier's precisely-typed builder: column names are judged
300
- * EAGERLY (at `closed(name, columns)`, before any axioms arrive the
301
- * macro-expansion analog), and the returned `withAxioms` reads its handle
302
- * set off the axioms record's own keys.
334
+ * first (the macro-expansion analog), then the handle set is read off the
335
+ * axioms record's own keys.
303
336
  */
304
- function closedPayload<Name extends string, Cols extends Record<string, PayloadField>>(
337
+ function closedPayload<Name extends string, Handles extends string, Cols extends PayloadColumns>(
305
338
  name: Name,
306
- columns: Cols
307
- ): <Handles extends string>(axioms: Axioms<Handles, Cols>) => Closed<Name, Handles, Cols> {
339
+ columns: Cols,
340
+ axioms: Axioms<Handles, Cols>
341
+ ): Closed<Name, Handles, Cols> {
308
342
  for (const columnName of Object.keys(columns)) {
309
343
  assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
310
344
  }
311
- return function withAxioms<Handles extends string>(axioms: Axioms<Handles, Cols>): Closed<Name, Handles, Cols> {
312
- const handles = Object.keys(axioms)
313
- for (const handle of handles) {
314
- assertDeclarationOrderKey(`closed relation ${name} handle`, handle)
315
- }
316
- if (!handleKeysOwn(axioms, handles)) {
317
- throw errors.new(`closed relation ${name}: handle enumeration incomplete`)
318
- }
319
- return mintClosed<Name, Handles, Cols>(name, handles, columns, axioms)
345
+ const handles = Object.keys(axioms)
346
+ for (const handle of handles) {
347
+ assertDeclarationOrderKey(`closed relation ${name} handle`, handle)
320
348
  }
349
+ if (!handleKeysOwn(axioms, handles)) {
350
+ throw errors.new(`closed relation ${name}: handle enumeration incomplete`)
351
+ }
352
+ return mintClosed<Name, Handles, Cols>(name, handles, columns, axioms)
353
+ }
354
+
355
+ /**
356
+ * The trusted seam of the ergonomic-surface mint: `where` reads back as an
357
+ * own function exactly when payload columns exist, and is ABSENT otherwise
358
+ * — the runtime twin of the {@link Closed} type's conditional arm
359
+ * (`ClosedCore` alone vs `ClosedCore & ClosedSelectable`), verified before
360
+ * the minted value is admitted at the conditional type.
361
+ */
362
+ function surfaceMinted<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
363
+ value: ClosedCore<Name, Handles, Cols>,
364
+ cols: readonly ClosedColumn[]
365
+ ): value is ClosedCore<Name, Handles, Cols> & Closed<Name, Handles, Cols> {
366
+ const selectable = "where" in value && typeof value.where === "function"
367
+ return cols.length > 0 ? selectable : !selectable
321
368
  }
322
369
 
323
370
  /**
324
371
  * Mints one closed relation value — the shared seam of both tiers, HONESTLY
325
372
  * typed end to end (a wrong-shaped mint is a compile error here, not a
326
373
  * laundered `unknown`): roster checks, eager axiom lowering, the
327
- * domain-labeled `id` descriptor, the frozen `columns` carrier (the runtime
328
- * twin of the `Cols` type parameter), and the handle constants.
374
+ * roster-carrying `id` descriptor, the frozen `columns` carrier (the runtime
375
+ * twin of the `Cols` type parameter), and — on the payload tier only — the
376
+ * ψ-selection `where()`.
329
377
  */
330
378
  function mintClosed<Name extends string, Handles extends string, Cols extends Record<string, PayloadField>>(
331
379
  name: Name,
@@ -342,17 +390,17 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
342
390
  throw errors.new(`closed relation ${name}: duplicate handle ${handle}`)
343
391
  }
344
392
  seen.add(handle)
345
- if (reservedHandleNames.includes(handle)) {
346
- throw errors.new(
347
- `closed relation ${name}: handle ${handle} collides with the closed value's own surface (${reservedHandleNames.join(", ")})`
348
- )
349
- }
350
393
  }
351
394
  const handleList: readonly Handles[] = Object.freeze([...handles])
352
- const roster: ClosedRoster = Object.freeze({ name, handles: handleList })
395
+ const roster: ClosedRoster<Handles> = Object.freeze({ name, handles: handleList })
353
396
  const cols: ClosedColumn[] = []
354
397
  for (const [columnName, field] of Object.entries(columns)) {
355
398
  assertDeclarationOrderKey(`closed relation ${name} column`, columnName)
399
+ if (columnName === "id") {
400
+ throw errors.new(
401
+ `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)`
402
+ )
403
+ }
356
404
  cols.push(Object.freeze({ name: columnName, field }))
357
405
  }
358
406
  Object.freeze(cols)
@@ -369,38 +417,58 @@ function mintClosed<Name extends string, Handles extends string, Cols extends Re
369
417
  columns: cols,
370
418
  rows: Object.freeze(rows)
371
419
  })
372
- const id: ClosedIdField<`${Name}Id`> = Object.freeze({
373
- kind: "u64",
374
- domain: `${name}Id`,
375
- closed: roster
376
- })
420
+ const id: ClosedIdField<Handles> = Object.freeze({ kind: "u64", closed: roster })
377
421
  /**
378
- * Handle names are arbitrary identifiers, so rows and constants are
379
- * minted with OWN-property definition (inside {@link mintAxioms} and
380
- * {@link mintHandleConstants}), never assignment: a handle named
381
- * "__proto__" would otherwise ride the Object.prototype accessor
382
- * silently swapping the record's prototype instead of creating the row,
383
- * and no-oping the constant (a primitive through the setter) — minting a
384
- * value whose type claims a bigint constant but reads back an object.
385
- * (Object SPREAD is CreateDataProperty by spec, so the copies below are
386
- * own-property safe for column names too.)
422
+ * Handle names are arbitrary identifiers, so axiom rows are minted with
423
+ * OWN-property definition (inside {@link mintAxioms}), never assignment:
424
+ * a handle named "__proto__" would otherwise ride the Object.prototype
425
+ * accessor silently swapping the record's prototype instead of
426
+ * creating the row. (Object SPREAD is CreateDataProperty by spec, so the
427
+ * copies below are own-property safe for column names too.)
387
428
  */
388
429
  const axiomsOut = mintAxioms<Handles, Cols>(name, handleList, cols, axioms)
389
- const constants = mintHandleConstants(name, handleList)
390
430
  const columnsOut: Cols = { ...columns }
391
431
  Object.freeze(columnsOut)
392
- return Object.freeze({
393
- ...constants,
394
- name,
395
- id,
396
- data,
397
- axioms: axiomsOut,
398
- columns: columnsOut,
399
- fromId(idValue: bigint): Handles | undefined {
400
- return handleList[Number(idValue)]
432
+ const holder: { value: Closed<Name, Handles, Cols> | undefined } = { value: undefined }
433
+ /**
434
+ * The ψ selection: resolved against the declared payload columns through
435
+ * the ONE selection machine (`relation.ts::resolveSelection` — a
436
+ * `ClosedColumn` is structurally a `RelationField`), never pre-folded
437
+ * into an id set (the engine folds at validate).
438
+ */
439
+ function where(selection: ClosedSelectionInput<Cols>): SelectedClosed<Name, Handles, Cols> {
440
+ const owner = holder.value
441
+ if (owner === undefined) {
442
+ throw errors.new(`closed relation ${name}: self-reference read before construction completed`)
401
443
  }
402
- })
444
+ return Object.freeze({
445
+ relation: owner,
446
+ selection: resolveSelection(name, cols, Object.entries(selection))
447
+ })
448
+ }
449
+ const core = { name, id, data, axioms: axiomsOut, columns: columnsOut }
450
+ const value: ClosedCore<Name, Handles, Cols> =
451
+ cols.length > 0 ? Object.freeze({ ...core, where }) : Object.freeze(core)
452
+ if (!surfaceMinted<Name, Handles, Cols>(value, cols)) {
453
+ throw errors.new(`closed relation ${name}: ergonomic-surface minting incomplete`)
454
+ }
455
+ holder.value = value
456
+ return value
403
457
  }
404
458
 
405
- export type { AnyClosed, AxiomRow, Axioms, Closed, ClosedColumn, ClosedCore, ClosedData, ClosedRow, PayloadField }
459
+ export type {
460
+ AnyClosed,
461
+ AnySelectedClosed,
462
+ AxiomRow,
463
+ Axioms,
464
+ Closed,
465
+ ClosedColumn,
466
+ ClosedCore,
467
+ ClosedData,
468
+ ClosedRow,
469
+ ClosedSelectable,
470
+ ClosedSelectionInput,
471
+ PayloadField,
472
+ SelectedClosed
473
+ }
406
474
  export { closed }
package/src/db.ts CHANGED
@@ -35,7 +35,17 @@ import * as errors from "@superbuilders/errors"
35
35
  import type { Exhumed } from "#exhume.ts"
36
36
  import { exhumeStore } from "#exhume.ts"
37
37
  import { lower } from "#lower.ts"
38
- import { factOf, isFreshField, isMintedFresh, type KeyFact, keyRowOf, type Minted, recordOf, rowOf } from "#marshal.ts"
38
+ import {
39
+ factOf,
40
+ handleOf,
41
+ isFreshField,
42
+ isMintedFresh,
43
+ type KeyFact,
44
+ keyRowOf,
45
+ type Minted,
46
+ recordOf,
47
+ rowOf
48
+ } from "#marshal.ts"
39
49
 
40
50
  import type {
41
51
  DbHandle,
@@ -55,7 +65,7 @@ import type { Query } from "#query/lower.ts"
55
65
  import { lowerQuery } from "#query/lower.ts"
56
66
  import { decodeAnswers, wireParams } from "#query/run.ts"
57
67
  import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
58
- import type { AnyRelation, Fact, InsertFact } from "#relation.ts"
68
+ import type { AnyRelation, Fact, InsertFact, RelationField } from "#relation.ts"
59
69
  import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
60
70
  import type { KeyStatement, Statement } from "#statements.ts"
61
71
 
@@ -79,7 +89,10 @@ type DeclaredKeyFact<R extends AnyRelation, Projection extends readonly string[]
79
89
  /**
80
90
  * One offending fact of a violation: the cited relation's name (a member
81
91
  * of the schema's record) and the fact decoded to a named natural-value
82
- * object — partial exactly as the engine cites it.
92
+ * object — partial exactly as the engine cites it. Closed-referencing
93
+ * cells arrive as handle NAMES (the marshal bijection's read half), so the
94
+ * record and the violation's `canonical` string — which the engine already
95
+ * renders with handle names — agree on the one spelling.
83
96
  */
84
97
  interface OffendingFact<Rels extends SchemaRelations> {
85
98
  readonly relation: keyof Rels & string
@@ -525,6 +538,22 @@ function orientationOf(reversed: boolean | undefined): "written" | "mirrored" |
525
538
  return "written"
526
539
  }
527
540
 
541
+ /**
542
+ * The declared field descriptors a violation's offending cells decode
543
+ * through: an ordinary relation's declared fields; a closed relation's
544
+ * SEALED shape — the roster-carrying synthetic `id` (the member's own
545
+ * reference descriptor) plus its payload columns (a `ClosedColumn` is
546
+ * structurally a `RelationField`). This is how {@link openDb}'s
547
+ * `offendingFactOf` reaches the open-time descriptors: the cited relation
548
+ * name resolves to the schema member, and the member carries them.
549
+ */
550
+ function declaredFieldsOf(member: SchemaRelation): readonly RelationField[] {
551
+ if ("axioms" in member) {
552
+ return [{ name: "id", field: member.id }, ...member.data.columns]
553
+ }
554
+ return member.data.fields
555
+ }
556
+
528
557
  /** The id-resolution tables one open builds: relation entries by name, statement slots by id. */
529
558
  interface Tables {
530
559
  readonly relations: ReadonlyMap<string, RelationEntry>
@@ -705,12 +734,20 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
705
734
  }
706
735
 
707
736
  function offendingFactOf(fact: WireViolationFact): OffendingFact<Rels> {
708
- if (!isMemberName(fact.relation)) {
737
+ const entry = tables.relations.get(fact.relation)
738
+ if (entry === undefined || !isMemberName(fact.relation)) {
709
739
  throw errors.new(`bumbledb violation cites unknown relation ${fact.relation}`)
710
740
  }
741
+ const declared = declaredFieldsOf(entry.member)
711
742
  const decoded: Record<string, FactValue> = {}
712
743
  for (const cell of fact.fields) {
713
- decoded[cell.name] = cell.value
744
+ const cited = declared.find(function byName(candidate) {
745
+ return candidate.name === cell.name
746
+ })
747
+ decoded[cell.name] =
748
+ cited !== undefined && "closed" in cited.field
749
+ ? handleOf(`violation fact ${fact.relation} field ${cell.name}`, cited.field.closed, cell.value)
750
+ : cell.value
714
751
  }
715
752
  return Object.freeze({ relation: fact.relation, fact: Object.freeze(decoded) })
716
753
  }
@@ -1419,16 +1456,31 @@ process.once("exit", function closeCachedStores() {
1419
1456
  }
1420
1457
  })
1421
1458
 
1459
+ /**
1460
+ * The engine twin of the schema-level class wall, as a matchable value
1461
+ * (`errors.is`): the shared lowering rejected a spec whose statement pairs
1462
+ * faces with disagreeing newtype labels — the faces of a dependency agree
1463
+ * on their newtype, or neither carries one. UNREACHABLE through the typed
1464
+ * builder (the SDK computes every label from the laws, so its lowered
1465
+ * specs cohere by construction); a raw spec handed to the bridge is the
1466
+ * one road here, and the runtime referee that proves the engine judges
1467
+ * what the types claim.
1468
+ */
1469
+ const ErrNewtypeMismatch = errors.new(
1470
+ "bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
1471
+ )
1472
+
1422
1473
  /**
1423
1474
  * The one admission path both verbs share: canonical-path cache lookup
1424
1475
  * first (a hit returns the SAME `Db` value for the identical theory, a
1425
1476
  * typed fingerprint error for a different one, and a typed refusal for
1426
1477
  * `create` — the store a cache entry proves initialized is exactly what
1427
1478
  * create refuses). On a miss: lower the theory, run one bridge call, and
1428
- * wrap the two domain refusals — `schemaError` (spec resolution + schema
1429
- * validation, every issue in one message) and `fingerprintMismatch` (a
1430
- * different theory cannot open the store) into typed errors carrying the
1431
- * engine's message intact.
1479
+ * wrap the domain refusals — `schemaError` (spec resolution + schema
1480
+ * validation, every issue in one message), `newtypeMismatch` (the
1481
+ * coherence wall, {@link ErrNewtypeMismatch}), and `fingerprintMismatch`
1482
+ * (a different theory cannot open the store) — into typed errors carrying
1483
+ * the engine's message intact.
1432
1484
  */
1433
1485
  function admit<Rels extends SchemaRelations>(
1434
1486
  verb: "create" | "open",
@@ -1458,6 +1510,9 @@ function admit<Rels extends SchemaRelations>(
1458
1510
  return native.dbOpen(canonical, spec)
1459
1511
  })
1460
1512
  if (!opened.ok) {
1513
+ if (opened.kind === "newtypeMismatch") {
1514
+ throw errors.wrap(ErrNewtypeMismatch, `${verb} ${canonical}: ${opened.message}`)
1515
+ }
1461
1516
  throw errors.new(`bumbledb ${opened.kind} (${verb} ${canonical}): ${opened.message}`)
1462
1517
  }
1463
1518
  const manifest = bridged("fetch bumbledb manifest", function fetchManifest() {
@@ -1526,4 +1581,4 @@ export type {
1526
1581
  WitnessedWriteResult,
1527
1582
  WriteResult
1528
1583
  }
1529
- export { abandon, Db }
1584
+ export { abandon, Db, ErrNewtypeMismatch }