@bjornpagen/bumbledb 0.3.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 (66) hide show
  1. package/COOKBOOK.md +66 -46
  2. package/README.md +28 -15
  3. package/dist/closed.d.ts +50 -73
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +38 -109
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +4 -1
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +26 -3
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +39 -39
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js +7 -16
  14. package/dist/face.js.map +1 -1
  15. package/dist/fields.d.ts +28 -14
  16. package/dist/fields.d.ts.map +1 -1
  17. package/dist/fields.js +15 -14
  18. package/dist/fields.js.map +1 -1
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/marshal.d.ts +33 -6
  24. package/dist/marshal.d.ts.map +1 -1
  25. package/dist/marshal.js +67 -6
  26. package/dist/marshal.js.map +1 -1
  27. package/dist/query/atom.d.ts +59 -14
  28. package/dist/query/atom.d.ts.map +1 -1
  29. package/dist/query/atom.js +3 -0
  30. package/dist/query/atom.js.map +1 -1
  31. package/dist/query/lower.d.ts.map +1 -1
  32. package/dist/query/lower.js +251 -26
  33. package/dist/query/lower.js.map +1 -1
  34. package/dist/query/run.d.ts +15 -5
  35. package/dist/query/run.d.ts.map +1 -1
  36. package/dist/query/run.js +26 -6
  37. package/dist/query/run.js.map +1 -1
  38. package/dist/query/scope.d.ts +35 -13
  39. package/dist/query/scope.d.ts.map +1 -1
  40. package/dist/query/scope.js +16 -4
  41. package/dist/query/scope.js.map +1 -1
  42. package/dist/relation.d.ts +8 -7
  43. package/dist/relation.d.ts.map +1 -1
  44. package/dist/relation.js +32 -10
  45. package/dist/relation.js.map +1 -1
  46. package/dist/spec.d.ts +3 -2
  47. package/dist/spec.d.ts.map +1 -1
  48. package/dist/spec.js.map +1 -1
  49. package/dist/statements.d.ts +10 -4
  50. package/dist/statements.d.ts.map +1 -1
  51. package/dist/statements.js +79 -7
  52. package/dist/statements.js.map +1 -1
  53. package/package.json +2 -2
  54. package/src/closed.ts +72 -179
  55. package/src/db.ts +42 -5
  56. package/src/face.ts +34 -45
  57. package/src/fields.ts +46 -34
  58. package/src/index.ts +1 -2
  59. package/src/marshal.ts +74 -7
  60. package/src/query/atom.ts +58 -15
  61. package/src/query/lower.ts +301 -28
  62. package/src/query/run.ts +26 -6
  63. package/src/query/scope.ts +49 -15
  64. package/src/relation.ts +45 -17
  65. package/src/spec.ts +3 -2
  66. package/src/statements.ts +86 -7
package/src/face.ts CHANGED
@@ -11,11 +11,12 @@
11
11
  * positional: tuple order is preserved in the type, and the statement
12
12
  * constructors pair the two sides' tuples by arity ({@link SameArity}) AND
13
13
  * by structural shape ({@link SameShapes}) — every projected field's
14
- * kind/width/element triple is read off the schema type (the minimal
15
- * kernel: descriptors are pure structure) and compared positionwise. There
16
- * is no domain to compare at construction domains are LAW-BORN: the
17
- * statements themselves define the equivalence classes, and `schema()` is
18
- * where they aggregate and get judged (the one-generator-per-class wall).
14
+ * kind/width/element/roster quadruple is read off the schema type (the
15
+ * minimal kernel: descriptors are pure structure, and a closed reference's
16
+ * roster IS part of that structure) and compared positionwise. There is no
17
+ * domain to compare at construction domains are LAW-BORN: the statements
18
+ * themselves define the equivalence classes, and `schema()` is where they
19
+ * aggregate and get judged (the one-generator-per-class wall).
19
20
  */
20
21
 
21
22
  import * as errors from "@superbuilders/errors"
@@ -54,27 +55,6 @@ function faceParts(source: FaceSource): {
54
55
  return { owner: source, selection: emptySelection }
55
56
  }
56
57
 
57
- /**
58
- * A disjunctive literal set for a selection binding — `field == {A, B}`.
59
- * The signature of {@link oneOf} demands two leading literals, so the
60
- * one-element set (banned: it is the bare literal) and the empty set
61
- * (banned: it selects nothing) are unwritable.
62
- */
63
- interface OneOf<V> {
64
- readonly literals: readonly [V, V, ...V[]]
65
- }
66
-
67
- /**
68
- * Constructs a literal set (read disjunctively) for a `where()` binding.
69
- * Two leading arguments by signature: the degenerate sets have no spelling
70
- * (the canonical-utterance law, `docs/architecture/70-api.md`).
71
- */
72
- function oneOf<V>(first: V, second: V, ...rest: V[]): OneOf<V> {
73
- const literals: readonly [V, V, ...V[]] = [first, second, ...rest]
74
- Object.freeze(literals)
75
- return Object.freeze({ literals })
76
- }
77
-
78
58
  /** The relation a face projects from — ordinary or closed. */
79
59
  type FaceOwner = AnyRelation | AnyClosed
80
60
 
@@ -131,20 +111,28 @@ type FaceFields<S extends FaceSource> = S extends AnySelected
131
111
  : never
132
112
 
133
113
  /**
134
- * One descriptor's structural comparand: the kind/width/element triple —
135
- * exactly the structure the minimal kernel carries, compared exactly as the
136
- * engine's Q1 law pairs positions (`schema/validate.rs`): a `bytes` width is
137
- * bound (bytes<16> vs bytes<32> mismatch), while an INTERVAL width is FREE —
138
- * the pointwise judgments quantify over points, which carry an element
139
- * domain and not a width, so `interval(u64)` pairs with `interval(u64, 1n)`
140
- * (recipe 9's extent/slot mirrors, recipe 29's mixed-width zones) and the
141
- * width slot reads `undefined` for every interval. Elements stay bound:
142
- * u64-vs-i64 interval pairs still mismatch.
114
+ * One descriptor's structural comparand: the kind/width/element/roster
115
+ * quadruple — exactly the structure the minimal kernel carries. The first
116
+ * three slots compare exactly as the engine's Q1 law pairs positions
117
+ * (`schema/validate.rs`): a `bytes` width is bound (bytes<16> vs bytes<32>
118
+ * mismatch), while an INTERVAL width is FREE the pointwise judgments
119
+ * quantify over points, which carry an element domain and not a width, so
120
+ * `interval(u64)` pairs with `interval(u64, 1n)` (recipe 9's extent/slot
121
+ * mirrors, recipe 29's mixed-width zones) and the width slot reads
122
+ * `undefined` for every interval. Elements stay bound: u64-vs-i64 interval
123
+ * pairs still mismatch. The ROSTER slot is SDK-only structure (the engine's
124
+ * wire carries plain u64s): a closed reference contributes its handle
125
+ * union, every other kind `undefined`, so a plain u64 face cannot pair with
126
+ * a closed `[id]` face — the vocabulary's own descriptor (`Kind.id`) is the
127
+ * ONE spelling of a closed reference at this surface, and a bare column
128
+ * cannot alias a vocabulary through a declared law. The runtime twin is the
129
+ * statement constructors' roster-identity walk (`statements.ts`).
143
130
  */
144
131
  type ShapeOf<F extends AnyField> = readonly [
145
132
  F["kind"],
146
133
  F extends { readonly element: unknown } ? undefined : F extends { readonly width: infer W } ? W : undefined,
147
- F extends { readonly element: infer E } ? E : undefined
134
+ F extends { readonly element: infer E } ? E : undefined,
135
+ F extends { readonly closed: { readonly handles: readonly (infer H extends string)[] } } ? H : undefined
148
136
  ]
149
137
 
150
138
  /** One field's structural shape within a declared field block (`undefined` when the name is foreign). */
@@ -216,10 +204,11 @@ type SameArity<A extends AnyFace, B extends AnyFace> =
216
204
  * bijection, or window project structurally incompatible fields at any
217
205
  * position, this type is intersected into the second face's parameter and
218
206
  * names both shape tuples — a u64 face against a str face, a bytes width
219
- * mismatch, or an interval element mismatch is a COMPILE error.
207
+ * mismatch, an interval element mismatch, or a bare column against a
208
+ * closed reference (the roster slot) is a COMPILE error.
220
209
  */
221
210
  interface FaceShapeMismatch<Left, Right> {
222
- readonly "face shape mismatch — positionwise kind, width, and element must be equal on both sides": readonly [
211
+ readonly "face shape mismatch — positionwise kind, width, element, and closed roster must be equal on both sides": readonly [
223
212
  Left,
224
213
  Right
225
214
  ]
@@ -229,11 +218,12 @@ interface FaceShapeMismatch<Left, Right> {
229
218
  * Resolves to `unknown` (a no-op intersection) when the two faces project
230
219
  * positionwise-equal structural shapes, and to {@link FaceShapeMismatch}
231
220
  * otherwise. Equality is mutual tuple assignability over the
232
- * kind/width/element triples. This is the whole construction-time wall —
233
- * deliberately: there is no domain to compare here. The domain wall lives
234
- * where domains are BORN: `schema()` computes every field's class from the
235
- * statement list and holds the one-generator-per-class law, and query
236
- * joins compare class names off the schema type.
221
+ * kind/width/element/roster quadruples. This is the whole
222
+ * construction-time wall — deliberately: there is no domain to compare
223
+ * here (the roster is descriptor STRUCTURE, not a domain). The domain wall
224
+ * lives where domains are BORN: `schema()` computes every field's class
225
+ * from the statement list and holds the one-generator-per-class law, and
226
+ * query joins compare class names off the schema type.
237
227
  */
238
228
  type SameShapes<A extends AnyFace, B extends AnyFace> =
239
229
  FaceShapes<A> extends FaceShapes<B>
@@ -323,9 +313,8 @@ export type {
323
313
  FaceShapeMismatch,
324
314
  FaceShapes,
325
315
  FaceSource,
326
- OneOf,
327
316
  OwnerOf,
328
317
  SameArity,
329
318
  SameShapes
330
319
  }
331
- export { on, oneOf, renderFace }
320
+ export { on, renderFace }
package/src/fields.ts CHANGED
@@ -50,11 +50,15 @@ function span(start: bigint, end: bigint): IntervalValue {
50
50
  * A closed relation's roster as seen from a referencing field: the handle
51
51
  * namespace `where()` selections and ground axioms resolve bare handle ids
52
52
  * through (the macro's own rule: a handle is legal exactly on a field that
53
- * references a closed relation).
53
+ * references a closed relation). The handle union is PRECISE — `H` carries
54
+ * the literal handle names in declaration order (the unbound `string`
55
+ * default exists only as the fallback where no roster is in scope); the
56
+ * runtime twin is the same frozen declaration-order array that was always
57
+ * there.
54
58
  */
55
- interface ClosedRoster {
59
+ interface ClosedRoster<H extends string = string> {
56
60
  readonly name: string
57
- readonly handles: readonly string[]
61
+ readonly handles: readonly H[]
58
62
  }
59
63
 
60
64
  /** The `bool` field descriptor: value type `boolean`. No `.fresh` (macro parity). */
@@ -125,14 +129,16 @@ interface IntervalField<
125
129
 
126
130
  /**
127
131
  * A closed relation's reference field descriptor (`Kind.id`) — a u64
128
- * descriptor carrying the closed linkage: the roster resolves bare handle
129
- * ids in selections and ground axioms, and `schema()` names the id's
130
- * generator class `"Kind.id"`. Terminal: no `.fresh` a vocabulary's rows
131
- * are ground axioms, never minted.
132
+ * descriptor carrying the closed linkage: the roster resolves handle
133
+ * literals in selections and ground axioms, and `schema()` names the id's
134
+ * generator class `"Kind.id"`. The handle union `H` is the field's VALUE
135
+ * TYPE (see {@link Infer}); `kind: "u64"` stays load-bearing for the class
136
+ * map and JoinOk, which compare kind/class/width/element. Terminal: no
137
+ * `.fresh` — a vocabulary's rows are ground axioms, never minted.
132
138
  */
133
- interface ClosedIdField {
139
+ interface ClosedIdField<H extends string = string> {
134
140
  readonly kind: "u64"
135
- readonly closed: ClosedRoster
141
+ readonly closed: ClosedRoster<H>
136
142
  }
137
143
 
138
144
  /** Any field descriptor, whatever its kind or marks. */
@@ -142,21 +148,28 @@ type AnyField = BoolField | StrField | U64Field | FreshU64Field | I64Field | Byt
142
148
  * The bare structural VALUE type of a field descriptor — the one total
143
149
  * definition every fact, result row, and query term reads: `bool` →
144
150
  * `boolean`, `str` → `string`, `u64`/`i64` → `bigint`, `bytes<N>` →
145
- * `Uint8Array`, intervals → {@link IntervalValue}.
151
+ * `Uint8Array`, intervals → {@link IntervalValue}, and a closed reference →
152
+ * its PRECISE handle union (`"DirectPass" | "Failed"` — the string-literal
153
+ * union IS the value type at the TS surface; the engine keeps u64 row ids
154
+ * and the marshal owns the bijection). The closed arm precedes the `u64`
155
+ * arm because a closed reference is structurally a u64 descriptor plus the
156
+ * roster.
146
157
  */
147
158
  type Infer<F extends AnyField> = F extends { readonly kind: "bool" }
148
159
  ? boolean
149
160
  : F extends { readonly kind: "str" }
150
161
  ? string
151
- : F extends { readonly kind: "u64" }
152
- ? bigint
153
- : F extends { readonly kind: "i64" }
162
+ : F extends { readonly closed: { readonly handles: readonly (infer H extends string)[] } }
163
+ ? H
164
+ : F extends { readonly kind: "u64" }
154
165
  ? bigint
155
- : F extends { readonly kind: "bytes" }
156
- ? Uint8Array
157
- : F extends { readonly kind: "interval" }
158
- ? IntervalValue
159
- : never
166
+ : F extends { readonly kind: "i64" }
167
+ ? bigint
168
+ : F extends { readonly kind: "bytes" }
169
+ ? Uint8Array
170
+ : F extends { readonly kind: "interval" }
171
+ ? IntervalValue
172
+ : never
160
173
 
161
174
  /**
162
175
  * The typed shape refusal of the selection-literal machine — reached only
@@ -179,22 +192,22 @@ function isIntervalLiteral(value: unknown): value is IntervalValue {
179
192
  }
180
193
 
181
194
  /**
182
- * Resolves one closed-handle literal: the handle id (a bare bigint) back to
183
- * its handle NAME through the roster an out-of-roster id is a
184
- * construction error, the belt the type level deliberately does not provide
185
- * (structural values make any bigint spellable here; the roster judges).
195
+ * Resolves one closed-handle literal: the handle NAME, verified against the
196
+ * roster an unknown name is a construction error, the belt the wide
197
+ * fallback type deliberately does not provide (structural values make any
198
+ * string spellable here; the roster judges). The name IS the value at the
199
+ * TS surface (the drizzle law); the wire literal already crossed as
200
+ * `{ kind: "handle", handle }`, so the output — and every fingerprint
201
+ * derived from it — is untouched.
186
202
  */
187
203
  function handleLiteral(closed: ClosedRoster, value: unknown): LiteralSpec {
188
- if (typeof value !== "bigint") {
189
- throw literalShapeError(`a ${closed.name} handle id (bigint)`, value)
204
+ if (typeof value !== "string") {
205
+ throw literalShapeError(`a ${closed.name} handle name (string)`, value)
190
206
  }
191
- const handle = closed.handles[Number(value)]
192
- if (handle === undefined) {
193
- throw errors.new(
194
- `closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
195
- )
207
+ if (!closed.handles.includes(value)) {
208
+ throw errors.new(`"${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`)
196
209
  }
197
- return { kind: "handle", handle }
210
+ return { kind: "handle", handle: value }
198
211
  }
199
212
 
200
213
  /** Lowers one interval literal at its element type. */
@@ -288,10 +301,9 @@ function interval(element: U64Field | I64Field, width?: bigint): IntervalField<"
288
301
  * Lowers one host literal at its field position to the wire
289
302
  * {@link LiteralSpec} — the selection-literal machine ground axioms and
290
303
  * `where()` bindings both ride (one machine, same errors — the macro's own
291
- * rule). A value on a closed-reference field resolves to its handle NAME
292
- * (the id is verified against the roster: an out-of-roster id is a
293
- * construction error); everything else lowers to a plain value tagged by
294
- * the field's structural kind.
304
+ * rule). A value on a closed-reference field IS its handle NAME (verified
305
+ * against the roster: an unknown name is a construction error); everything
306
+ * else lowers to a plain value tagged by the field's structural kind.
295
307
  */
296
308
  function literalOf(field: AnyField, value: unknown): LiteralSpec {
297
309
  if ("closed" in field) {
package/src/index.ts CHANGED
@@ -75,12 +75,11 @@ export type {
75
75
  FaceShapeMismatch,
76
76
  FaceShapes,
77
77
  FaceSource,
78
- OneOf,
79
78
  OwnerOf,
80
79
  SameArity,
81
80
  SameShapes
82
81
  } from "#face.ts"
83
- export { on, oneOf } from "#face.ts"
82
+ export { on } from "#face.ts"
84
83
  export type {
85
84
  AnyField,
86
85
  BoolField,
package/src/marshal.ts CHANGED
@@ -13,10 +13,22 @@
13
13
  * the engine admitted IS a legal fact of its relation (the same trust
14
14
  * direction as Rust's typed readback). Shape mismatches here are genuine
15
15
  * failures and THROW typed; they are never domain data.
16
+ *
17
+ * THE CLOSED BIJECTION (0.4.0): a closed-referencing cell crosses this
18
+ * boundary as its handle NAME — the write side lowers name → u64 row id
19
+ * (declaration order = row ids, the sealed roster's own law, ≤ 256 rows),
20
+ * the read side lifts id → name, and both directions are total and static
21
+ * over the roster. An unknown name is a pointed THROW at the write seam —
22
+ * a deliberate UPGRADE over 0.3.0, where any bigint sailed through the
23
+ * marshal to a commit-time containment violation; the wrong spelling now
24
+ * dies here, before the engine ever sees the row. An out-of-roster id on
25
+ * the read side (reachable only in a store whose closed-typed column was
26
+ * never pinned by its containment law) is equally pointed — never a
27
+ * silent fallback, never `undefined`.
16
28
  */
17
29
 
18
30
  import * as errors from "@superbuilders/errors"
19
- import type { AnyField } from "#fields.ts"
31
+ import type { AnyField, ClosedRoster } from "#fields.ts"
20
32
  import type { FactValue } from "#native.ts"
21
33
  import type { AnyRelation, Fact, FreshKeys, RelationData } from "#relation.ts"
22
34
 
@@ -80,14 +92,64 @@ function recordOf(fact: object): Record<string, unknown> {
80
92
  return Object.fromEntries(Object.entries(fact))
81
93
  }
82
94
 
95
+ /**
96
+ * The write half of the closed bijection: one handle NAME to its u64 row
97
+ * id (declaration order = row ids — the engine's own minting of the
98
+ * sealed extension). An unknown name is a pointed refusal naming the
99
+ * vocabulary and its roster — the 0.4.0 upgrade over any-bigint-compiles:
100
+ * the wrong spelling dies at the marshal, never as a commit-time
101
+ * violation. `indexOf` is the whole machine (the roster is ≤ 256 rows,
102
+ * engine law — no map is warranted).
103
+ */
104
+ function closedCellOf(context: string, closed: ClosedRoster, name: string): FactValue {
105
+ const id = closed.handles.indexOf(name)
106
+ if (id === -1) {
107
+ throw errors.new(
108
+ `${context}: "${name}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
109
+ )
110
+ }
111
+ return BigInt(id)
112
+ }
113
+
114
+ /**
115
+ * The read half of the closed bijection: one u64 row id back to its handle
116
+ * NAME (`Number(cell)` is safe — the sealed extension holds at most 256
117
+ * rows, engine law). An id outside the roster THROWS pointed, never a
118
+ * silent fallback and never `undefined`: the state is reachable only in a
119
+ * store whose closed-typed column was never pinned by its containment law,
120
+ * and the error names that missing piece.
121
+ */
122
+ function handleOf(context: string, closed: ClosedRoster, cell: FactValue): string {
123
+ if (typeof cell !== "bigint") {
124
+ throw cellShapeError(context, `a ${closed.name} handle id (bigint)`, cell)
125
+ }
126
+ const handle = closed.handles[Number(cell)]
127
+ if (handle === undefined) {
128
+ throw errors.new(
129
+ `${context}: id ${cell} is outside the ${closed.name} roster (${closed.handles.join(", ")}) — the column types ${closed.name} but no law pins it — a containment statement is the missing piece`
130
+ )
131
+ }
132
+ return handle
133
+ }
134
+
83
135
  /**
84
136
  * Marshals one host cell at its field position to the natural wire value,
85
137
  * schema-directed by the field descriptor's structural kind (never
86
- * guessed). Values are bare, so the runtime values ARE the wire's natural
87
- * JS values; widths and domain labels are the engine's own judgment at the
88
- * write boundary.
138
+ * guessed). A closed-referencing cell arrives as its handle NAME and
139
+ * lowers through {@link closedCellOf} the arm precedes the switch
140
+ * because a closed reference is structurally a u64 descriptor plus the
141
+ * roster (the same precedence `Infer` pins at the type level). Everything
142
+ * else is bare, so the runtime values ARE the wire's natural JS values;
143
+ * widths and domain labels are the engine's own judgment at the write
144
+ * boundary.
89
145
  */
90
146
  function cellOf(context: string, field: AnyField, value: unknown): FactValue {
147
+ if ("closed" in field) {
148
+ if (typeof value !== "string") {
149
+ throw cellShapeError(context, `a ${field.closed.name} handle name (string)`, value)
150
+ }
151
+ return closedCellOf(context, field.closed, value)
152
+ }
91
153
  switch (field.kind) {
92
154
  case "bool": {
93
155
  if (typeof value !== "boolean") {
@@ -210,7 +272,9 @@ function isMintedFresh<R extends AnyRelation>(
210
272
  /**
211
273
  * Unmarshals one positional row to the relation's named, frozen fact object
212
274
  * of bare structural values — the inverse of {@link rowOf},
213
- * ordinal-directed by the same declaration order.
275
+ * ordinal-directed by the same declaration order. Closed-referencing cells
276
+ * lift id → handle NAME through {@link handleOf} (the read half of the
277
+ * bijection), so every fact a user sees speaks the roster's vocabulary.
214
278
  */
215
279
  function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]): Fact<R> {
216
280
  const data = relation.data
@@ -225,7 +289,10 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
225
289
  if (cell === undefined) {
226
290
  throw errors.new(`relation ${data.name}: row cell ${ordinal} (${declared.name}) is absent`)
227
291
  }
228
- decoded[declared.name] = cell
292
+ decoded[declared.name] =
293
+ "closed" in declared.field
294
+ ? handleOf(`relation ${data.name} field ${declared.name}`, declared.field.closed, cell)
295
+ : cell
229
296
  })
230
297
  Object.freeze(decoded)
231
298
  if (!isCompleteFact(relation, decoded)) {
@@ -235,4 +302,4 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
235
302
  }
236
303
 
237
304
  export type { KeyFact, Minted }
238
- export { cellOf, factOf, isFreshField, isMintedFresh, keyRowOf, recordOf, rowOf }
305
+ export { cellOf, factOf, handleOf, isFreshField, isMintedFresh, keyRowOf, recordOf, rowOf }
package/src/query/atom.ts CHANGED
@@ -4,6 +4,9 @@
4
4
  * (`bumbledb/crates/bumbledb/src/ir.rs`, the bijection target;
5
5
  * `docs/architecture/20-query-ir.md` normative). A `match` binding record
6
6
  * binds fields to vars, params, ∈-set params, or bare structural literals
7
+ * — a closed-reference field's literal is its handle NAME, and a plain
8
+ * ARRAY of names there is membership, folded into the program (closed-only
9
+ * by owner ruling; see {@link BindingInput})
7
10
  * (unmentioned fields ARE the wildcard — no wildcard value exists);
8
11
  * `not(Rel, {...})` is negation-as-position (anti-join); `eq`/`ne` and the
9
12
  * order roster, `pointIn`/`covers` (both spellings of `ir::CmpOp::PointIn`,
@@ -26,7 +29,7 @@
26
29
 
27
30
  import * as errors from "@superbuilders/errors"
28
31
  import type { AnyClosed } from "#closed.ts"
29
- import type { AnyField, ClosedIdField, Infer, IntervalValue } from "#fields.ts"
32
+ import type { AnyField, ClosedIdField, ClosedRoster, Infer, IntervalValue } from "#fields.ts"
30
33
  import type { ClassLookup, ClassRecordOf, SchemaClasses } from "#law.ts"
31
34
  import type {
32
35
  ClassedField,
@@ -55,23 +58,32 @@ type MatchOwner = AnyRelation | AnyClosed
55
58
  /**
56
59
  * The matchable field block of an atom owner: a relation's declared
57
60
  * fields; a closed relation's SEALED shape — the synthetic `id` (the
58
- * roster-carrying descriptor) first, then the declared payload columns
59
- * read through the typed `columns` carrier (the one source of payload
60
- * typing no parallel column table exists). The runtime twin is
61
- * `matchFieldsOf` in `#query/lower.ts`; the id-first ordinal shift the two
62
- * tiers share is pinned by the lowering golden.
61
+ * value's OWN roster-carrying descriptor, at its precise type: the handle
62
+ * union rides into ψ id bindings and joins exactly as it does on a
63
+ * referencing column) first, then the declared payload columns read
64
+ * through the typed `columns` carrier (the one source of payload typing —
65
+ * no parallel column table exists). The runtime twin is `matchFieldsOf` in
66
+ * `#query/lower.ts`; the id-first ordinal shift the two tiers share is
67
+ * pinned by the lowering golden.
63
68
  */
64
69
  type MatchFields<R extends MatchOwner> = R extends AnyClosed
65
- ? { readonly id: ClosedIdField } & R["columns"]
70
+ ? { readonly id: R["id"] } & R["columns"]
66
71
  : R extends AnyRelation
67
72
  ? RelationFields<R>
68
73
  : never
69
74
 
70
- /** One atom-binding position as runtime data. */
75
+ /**
76
+ * One atom-binding position as runtime data. `literalSet` is a membership
77
+ * ARRAY at a closed-reference field, folded into the program: `name` is
78
+ * the content-addressed registry key its dense `ParamId` is minted under
79
+ * (the lowering rides the existing param-set term; the SDK itself supplies
80
+ * the translated member set at every execute — never the host).
81
+ */
71
82
  type BindingTermData =
72
83
  | { readonly kind: "var"; readonly name: string }
73
84
  | { readonly kind: "param"; readonly name: string }
74
85
  | { readonly kind: "setParam"; readonly name: string }
86
+ | { readonly kind: "literalSet"; readonly name: string; readonly members: readonly string[] }
75
87
  | { readonly kind: "literal"; readonly value: unknown }
76
88
 
77
89
  /** One resolved binding: the field's name, its descriptor, its law-computed class, and the term. */
@@ -135,10 +147,18 @@ type SelectEntryData =
135
147
  | { readonly kind: "measure"; readonly over: string }
136
148
  | { readonly kind: "aggregate"; readonly agg: AggData }
137
149
 
138
- /** One answer column: its name (the row object key) and its entry. */
150
+ /**
151
+ * One answer column: its name (the row object key), its entry, and — when
152
+ * the column's value is a closed reference (a projected var or an
153
+ * Arg-carried payload bound at a closed-referencing field) — the roster the
154
+ * decode lifts row ids back to handle NAMES through (the read half of the
155
+ * marshal bijection; `undefined` on every bare column). The slice is
156
+ * SDK-side marshaling data only: the wire `ProgramIr` never carries it.
157
+ */
139
158
  interface SelectColumn {
140
159
  readonly name: string
141
160
  readonly entry: SelectEntryData
161
+ readonly closed: ClosedRoster | undefined
142
162
  }
143
163
 
144
164
  /** One body item of a rule, in written order. */
@@ -151,13 +171,17 @@ type RuleItem =
151
171
  /**
152
172
  * One use of a parameter inside a rule, in written order: the census the
153
173
  * query-level registry folds (first use mints the dense `ParamId`, first
154
- * FIELD-ANCHORED use types the wire).
174
+ * FIELD-ANCHORED use types the wire). `members` is present exactly on a
175
+ * membership-array use (a literal set folded into the program): the handle
176
+ * names the SDK itself translates and supplies at execute — the entry
177
+ * never appears in the host's params object.
155
178
  */
156
179
  interface ParamUse {
157
180
  readonly name: string
158
181
  readonly shape: "value" | "set" | "mask"
159
182
  readonly anchor: AnyField | "measure" | undefined
160
183
  readonly op: "binding" | CmpKind
184
+ readonly members: readonly string[] | undefined
161
185
  }
162
186
 
163
187
  /** One complete rule as runtime data. */
@@ -184,10 +208,18 @@ interface RecData {
184
208
  * of the field's value type, a var/param/∈-set-param term — and, when the
185
209
  * field is interval-typed, a bare point literal (the IR's membership
186
210
  * typing rule: an element-typed term at an interval field is point
187
- * membership; an interval-typed term is value equality).
211
+ * membership; an interval-typed term is value equality). A
212
+ * CLOSED-reference field additionally takes a plain ARRAY of handle names
213
+ * read as membership — `kind: ["Practice", "Review"]` (the drizzle law:
214
+ * set membership is an array, never an operator). Arrays are CLOSED-ONLY
215
+ * in this packet by owner ruling: ordinary u64/str membership already has
216
+ * its spelling through `r.inSet` params; widening literal arrays to every
217
+ * literal-capable kind is a separate future taste call — deliberately not
218
+ * done here.
188
219
  */
189
220
  type BindingInput<F extends AnyField> =
190
221
  | Infer<F>
222
+ | (F extends ClosedIdField ? readonly Infer<F>[] : never)
191
223
  | (F extends { readonly kind: "interval" } ? bigint : never)
192
224
  | Var<string>
193
225
  | Param<string>
@@ -516,11 +548,22 @@ function not<R extends MatchOwner, const B extends MatchShape<MatchFields<R>>>(
516
548
  return Object.freeze(value)
517
549
  }
518
550
 
519
- /** Whether a var name is bound in the environment at an orderable (u64/i64) field. */
551
+ /**
552
+ * Whether a var name is bound in the environment at an orderable (u64/i64)
553
+ * field. A CLOSED reference is excluded even though its kind is `u64`: a
554
+ * vocabulary's declaration-id order is an accident, not semantics
555
+ * (`docs/architecture/10-data-model.md` § orderability — order on it is
556
+ * refused exactly as the enum's ordinal order was), so every
557
+ * order-comparison and fold position refuses closed-bound terms — this
558
+ * judgment is the one gate they all read, and the construction-time
559
+ * validations in `#query/lower.ts` are its runtime twin.
560
+ */
520
561
  type OrderVarOk<Env extends EnvShape, N extends string> = N extends keyof Env
521
- ? Env[N]["field"]["kind"] extends "u64" | "i64"
522
- ? true
523
- : false
562
+ ? Env[N]["field"] extends { readonly closed: ClosedRoster }
563
+ ? false
564
+ : Env[N]["field"]["kind"] extends "u64" | "i64"
565
+ ? true
566
+ : false
524
567
  : false
525
568
 
526
569
  /** Whether a var name is bound at an interval field. */