@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
@@ -26,8 +26,9 @@
26
26
  */
27
27
 
28
28
  import * as errors from "@superbuilders/errors"
29
- import type { AnyField } from "#fields.ts"
29
+ import type { AnyField, ClosedRoster } from "#fields.ts"
30
30
  import { assertDeclarationOrderKey } from "#fields.ts"
31
+ import type { ClassRecordOf, SchemaClasses } from "#law.ts"
31
32
  import type {
32
33
  AtomIr,
33
34
  ComparisonIr,
@@ -56,6 +57,8 @@ import type {
56
57
  CondData,
57
58
  CondParamsShape,
58
59
  MaskData,
60
+ MatchFields,
61
+ MatchOwner,
59
62
  MatchShape,
60
63
  ParamUse,
61
64
  RecData,
@@ -66,7 +69,16 @@ import type {
66
69
  TreeData
67
70
  } from "#query/atom.ts"
68
71
  import { allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
69
- import type { EnvShape, Flatten, InferredOf, JoinOk, ParamEntry, ParamsRecord, Var } from "#query/scope.ts"
72
+ import type {
73
+ ClassedField,
74
+ EnvShape,
75
+ Flatten,
76
+ InferredOf,
77
+ JoinOk,
78
+ ParamEntry,
79
+ ParamsRecord,
80
+ Var
81
+ } from "#query/scope.ts"
70
82
  import {
71
83
  fieldJoins,
72
84
  inferred,
@@ -76,20 +88,27 @@ import {
76
88
  makeParam,
77
89
  makeSetParam,
78
90
  makeVar,
91
+ makeVars,
79
92
  renderFieldKind,
80
93
  term
81
94
  } from "#query/scope.ts"
82
95
  import type { CheckNameSelect, CheckSelect, RowOfNameSelect, RowOfSelect, SelectEntry } from "#query/select.ts"
83
96
  import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts"
84
- import type { AnyRelation, FieldsShape, RelationFields } from "#relation.ts"
97
+ import type { FieldsShape, RelationField } from "#relation.ts"
85
98
  import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
86
99
 
87
- /** The ordinary (matchable) relations of a schema's record — closed relations lack the relation shape entirely. */
88
- type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
100
+ /**
101
+ * The matchable members of a schema's record — ordinary relations AND
102
+ * closed vocabularies (ψ query atoms: a closed atom is an ordinary EDB
103
+ * atom over the sealed extension; the ENGINE decides whether it folds to a
104
+ * plan-constant member set or joins the L1-resident virtual image — the
105
+ * SDK lowers pass-through and never knows which).
106
+ */
107
+ type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], MatchOwner>
89
108
 
90
- /** The environment after one bindings record: the incoming env plus every var the record binds. */
91
- type EnvOfMatch<Env extends EnvShape, F extends FieldsShape, B> =
92
- Flatten<Env & BindEnv<F, B>> extends infer E extends EnvShape ? E : never
109
+ /** The environment after one bindings record: the incoming env plus every var the record binds (as classed slots). */
110
+ type EnvOfMatch<Env extends EnvShape, F extends FieldsShape, CR, B> =
111
+ Flatten<Env & BindEnv<F, CR, B>> extends infer E extends EnvShape ? E : never
93
112
 
94
113
  /** Reads an inferred-params carrier off a rec reference or rule value. */
95
114
  type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends ParamsRecord } ? P : Record<never, never>
@@ -98,12 +117,13 @@ type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends Para
98
117
  type RowOf<T> = InferredOf<T> extends { readonly row: infer R } ? R : never
99
118
 
100
119
  /**
101
- * A recursive predicate's HEAD signature as field descriptors, position
102
- * for position — carried on the rec reference so an `idb` join can be
103
- * judged against it; `undefined` on values that carry no head (a plain
104
- * query rule, or an unthreaded rec handle before its first rule).
120
+ * A recursive predicate's HEAD signature as classed slots (descriptor +
121
+ * law-computed class), position for position — carried on the rec
122
+ * reference so an `idb` join can be judged against it; `undefined` on
123
+ * values that carry no head (a plain query rule, or an unthreaded rec
124
+ * handle before its first rule).
105
125
  */
106
- type HeadShape = readonly AnyField[] | undefined
126
+ type HeadShape = readonly ClassedField[] | undefined
107
127
 
108
128
  /**
109
129
  * One finished rule as a plain value: the runtime data plus the inferred
@@ -125,7 +145,7 @@ type HeadFieldsOf<Env extends EnvShape, S extends readonly string[]> = {
125
145
  }
126
146
 
127
147
  /** Reads an inferred-head carrier off a rule value or rec reference. */
128
- type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends readonly AnyField[] } ? H : undefined
148
+ type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends readonly ClassedField[] } ? H : undefined
129
149
 
130
150
  /**
131
151
  * A recursive predicate REFERENCE — the shape `idb()` targets carry: the
@@ -142,11 +162,11 @@ interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadS
142
162
  readonly [inferred]?: { readonly params: P; readonly head: Head }
143
163
  }
144
164
 
145
- /** One `idb` position's judgment: the var must be bound by a relation atom, domain-equal to the head column when the head is carried. */
165
+ /** One `idb` position's judgment: the var must be bound by a relation atom, class-equal to the head slot when the head is carried. */
146
166
  type IdbVarOk<Env extends EnvShape, T, F> =
147
167
  T extends Var<infer N extends string>
148
168
  ? N extends keyof Env
149
- ? F extends AnyField
169
+ ? F extends ClassedField
150
170
  ? JoinOk<Env[N], F>
151
171
  : true
152
172
  : false
@@ -156,12 +176,12 @@ type IdbVarOk<Env extends EnvShape, T, F> =
156
176
  * The validated `idb` variable tuple: every var must already be bound by a
157
177
  * relation atom of the rule; and when the target carries its head
158
178
  * signature (the threaded rec handle), the tuple must match the head's
159
- * arity and every position must be domain-equal to its head column — the
179
+ * arity and every position must be class-equal to its head slot — the
160
180
  * same wall `JoinOk` holds for EDB atoms. An unthreaded handle carries no
161
- * head; its joins stay boundness-checked here and arity/domain-judged at
181
+ * head; its joins stay boundness-checked here and arity/class-judged at
162
182
  * prepare (the engine's law stands behind both tiers).
163
183
  */
164
- type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> = Head extends readonly AnyField[]
184
+ type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> = Head extends readonly ClassedField[]
165
185
  ? V extends readonly unknown[]
166
186
  ? V["length"] extends Head["length"]
167
187
  ? { readonly [I in keyof V]: IdbVarOk<Env, V[I], Head[I & keyof Head]> extends true ? V[I] : never }
@@ -178,6 +198,8 @@ type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> =
178
198
  interface TermOps {
179
199
  /** Declares/names one variable: typed by the field it first binds; reuse joins. */
180
200
  readonly var: typeof makeVar
201
+ /** Mints several variables at once — `const { service, w } = r.vars("service", "w")`: each name typed exactly; duplicates refuse. */
202
+ readonly vars: typeof makeVars
181
203
  /** Names one scalar parameter: typed by its use; the key of the execute params object. */
182
204
  readonly param: typeof makeParam
183
205
  /** Names one ∈-set parameter (the IR's `ParamSet`): bound to a readonly array at execution. */
@@ -208,51 +230,77 @@ interface TermOps {
208
230
  readonly pack: typeof pack
209
231
  }
210
232
 
211
- /** The rule builder a `query(S).rule(...)` callback receives: the ops plus the first atom. */
212
- interface QueryRuleScope<Rels extends SchemaRelations> extends TermOps {
213
- /** The first EDB atom of the rule: fields bind vars, params, ∈-sets, or bare literals; absence is the wildcard (same-named vars within the record join domain-equal). */
214
- match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
233
+ /** The rule builder a `query(S).rule(...)` callback receives: the ops plus the first atom (`Classes` — the schema type's class map, the join judge's authority). */
234
+ interface QueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
235
+ /** The first EDB atom of the rule: fields bind vars, params, ∈-sets, or bare literals; absence is the wildcard (same-named vars within the record join class-equal). */
236
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
215
237
  relation: R,
216
- bindings: B & CheckBindings<Record<never, never>, RelationFields<R>, B>
217
- ): QueryRuleChain<Rels, EnvOfMatch<Record<never, never>, RelationFields<R>, B>, BindParamsShape<RelationFields<R>, B>>
238
+ bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
239
+ ): QueryRuleChain<
240
+ Rels,
241
+ EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
242
+ BindParamsShape<MatchFields<R>, B>,
243
+ Classes
244
+ >
218
245
  }
219
246
 
220
247
  /** The chain of a plain query rule: more atoms, residual predicates, then the head. */
221
- interface QueryRuleChain<Rels extends SchemaRelations, Env extends EnvShape, P extends ParamsRecord> {
222
- /** One more positive EDB atom — var reuse joins, domain-equal by the environment check. */
223
- match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
248
+ interface QueryRuleChain<
249
+ Rels extends SchemaRelations,
250
+ Env extends EnvShape,
251
+ P extends ParamsRecord,
252
+ Classes extends SchemaClasses = SchemaClasses
253
+ > {
254
+ /** One more positive EDB atom — var reuse joins, class-equal by the environment check. */
255
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
224
256
  relation: R,
225
- bindings: B & CheckBindings<Env, RelationFields<R>, B>
226
- ): QueryRuleChain<Rels, EnvOfMatch<Env, RelationFields<R>, B>, Flatten<P & BindParamsShape<RelationFields<R>, B>>>
257
+ bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
258
+ ): QueryRuleChain<
259
+ Rels,
260
+ EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
261
+ Flatten<P & BindParamsShape<MatchFields<R>, B>>,
262
+ Classes
263
+ >
227
264
  /** One residual predicate: a comparison, an `and`/`or` tree, or a negated atom (`r.not`). */
228
265
  where<const C extends AnyCond>(
229
- cond: CheckCond<Env, C> & C
230
- ): QueryRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>>
266
+ cond: CheckCond<Env, Classes, C> & C
267
+ ): QueryRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
231
268
  /** The head projection: var names, the measure, and aggregates; written order = answer column order. */
232
269
  select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
233
270
  }
234
271
 
235
272
  /** The rule builder an OUTPUT rule of a `program()` receives: a query rule plus finished-stratum `idb` atoms. */
236
- interface OutputRuleScope<Rels extends SchemaRelations> extends TermOps {
237
- match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
273
+ interface OutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
274
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
238
275
  relation: R,
239
- bindings: B & CheckBindings<Record<never, never>, RelationFields<R>, B>
276
+ bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
240
277
  ): OutputRuleChain<
241
278
  Rels,
242
- EnvOfMatch<Record<never, never>, RelationFields<R>, B>,
243
- BindParamsShape<RelationFields<R>, B>
279
+ EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
280
+ BindParamsShape<MatchFields<R>, B>,
281
+ Classes
244
282
  >
245
283
  }
246
284
 
247
285
  /** The chain of an output rule: atoms, predicates, `idb` joins over the program's recs, then the head. */
248
- interface OutputRuleChain<Rels extends SchemaRelations, Env extends EnvShape, P extends ParamsRecord> {
249
- match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
286
+ interface OutputRuleChain<
287
+ Rels extends SchemaRelations,
288
+ Env extends EnvShape,
289
+ P extends ParamsRecord,
290
+ Classes extends SchemaClasses = SchemaClasses
291
+ > {
292
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
250
293
  relation: R,
251
- bindings: B & CheckBindings<Env, RelationFields<R>, B>
252
- ): OutputRuleChain<Rels, EnvOfMatch<Env, RelationFields<R>, B>, Flatten<P & BindParamsShape<RelationFields<R>, B>>>
294
+ bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
295
+ ): OutputRuleChain<
296
+ Rels,
297
+ EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
298
+ Flatten<P & BindParamsShape<MatchFields<R>, B>>,
299
+ Classes
300
+ >
253
301
  where<const C extends AnyCond>(
254
- cond: CheckCond<Env, C> & C
255
- ): OutputRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>>
302
+ cond: CheckCond<Env, Classes, C> & C
303
+ ): OutputRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
256
304
  /**
257
305
  * One `idb` atom over a FINISHED stratum (any rec of this program): a
258
306
  * positional join against the rec's head. An idb atom is a join
@@ -260,26 +308,28 @@ interface OutputRuleChain<Rels extends SchemaRelations, Env extends EnvShape, P
260
308
  * the rule (the theory's own domain relation; the rec's answers are
261
309
  * theory values, so the join is identity). Threading the rec value the
262
310
  * last `.rule(...)` returned carries its rules' params into `Params`
263
- * AND its head signature, so the join is arity- and domain-checked
311
+ * AND its head signature, so the join is arity- and class-checked
264
312
  * against the head at compile time.
265
313
  */
266
314
  idb<Target extends RecRef<string, ParamsRecord>, const V extends readonly Var<string>[]>(
267
315
  target: Target,
268
316
  ...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
269
- ): OutputRuleChain<Rels, Env, Flatten<P & ParamsOf<Target>>>
317
+ ): OutputRuleChain<Rels, Env, Flatten<P & ParamsOf<Target>>, Classes>
270
318
  select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
271
319
  }
272
320
 
273
321
  /** The rule builder a RECURSIVE rule (`rec.rule(...)`) receives. */
274
- interface RecRuleScope<Rels extends SchemaRelations, Self extends string> extends TermOps {
275
- match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
322
+ interface RecRuleScope<Rels extends SchemaRelations, Self extends string, Classes extends SchemaClasses = SchemaClasses>
323
+ extends TermOps {
324
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
276
325
  relation: R,
277
- bindings: B & CheckBindings<Record<never, never>, RelationFields<R>, B>
326
+ bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
278
327
  ): RecRuleChain<
279
328
  Rels,
280
329
  Self,
281
- EnvOfMatch<Record<never, never>, RelationFields<R>, B>,
282
- BindParamsShape<RelationFields<R>, B>
330
+ EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
331
+ BindParamsShape<MatchFields<R>, B>,
332
+ Classes
283
333
  >
284
334
  }
285
335
 
@@ -295,21 +345,28 @@ interface RecRuleChain<
295
345
  Rels extends SchemaRelations,
296
346
  Self extends string,
297
347
  Env extends EnvShape,
298
- P extends ParamsRecord
348
+ P extends ParamsRecord,
349
+ Classes extends SchemaClasses = SchemaClasses
299
350
  > {
300
- match<R extends QueryRelation<Rels>, const B extends MatchShape<RelationFields<R>>>(
351
+ match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
301
352
  relation: R,
302
- bindings: B & CheckBindings<Env, RelationFields<R>, B>
303
- ): RecRuleChain<Rels, Self, EnvOfMatch<Env, RelationFields<R>, B>, Flatten<P & BindParamsShape<RelationFields<R>, B>>>
353
+ bindings: B & CheckBindings<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
354
+ ): RecRuleChain<
355
+ Rels,
356
+ Self,
357
+ EnvOfMatch<Env, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
358
+ Flatten<P & BindParamsShape<MatchFields<R>, B>>,
359
+ Classes
360
+ >
304
361
  where<const C extends AnyCond>(
305
- cond: CheckCond<Env, C> & C
306
- ): RecRuleChain<Rels, Self, Env, Flatten<P & CondParamsShape<Env, C>>>
307
- /** The self-recursive atom: `idb(self, ...boundVars)` — only this rec's own reference is accepted (threaded, its head arity- and domain-checks the join). */
362
+ cond: CheckCond<Env, Classes, C> & C
363
+ ): RecRuleChain<Rels, Self, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
364
+ /** The self-recursive atom: `idb(self, ...boundVars)` — only this rec's own reference is accepted (threaded, its head arity- and class-checks the join). */
308
365
  idb<Target extends RecRef<Self, ParamsRecord>, const V extends readonly Var<string>[]>(
309
366
  target: Target,
310
367
  ...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
311
- ): RecRuleChain<Rels, Self, Env, P>
312
- /** The recursive head: bound variable names only (the creation quarantine, restated for fixpoint topology); the value carries the head's field descriptors for `idb` pairing. */
368
+ ): RecRuleChain<Rels, Self, Env, P, Classes>
369
+ /** The recursive head: bound variable names only (the creation quarantine, restated for fixpoint topology); the value carries the head's classed slots for `idb` pairing. */
313
370
  select<const S extends readonly string[]>(
314
371
  ...names: CheckNameSelect<Env, S> & S
315
372
  ): RuleValue<RowOfNameSelect<Env, S>, P, HeadFieldsOf<Env, S>>
@@ -333,13 +390,18 @@ interface QueryData {
333
390
  * the rules use. Prepare with `db.prepare(q)`; nothing here touches an
334
391
  * engine.
335
392
  */
336
- interface Query<Rels extends SchemaRelations, Row, Params extends ParamsRecord> {
337
- readonly schema: Schema<Rels>
393
+ interface Query<
394
+ Rels extends SchemaRelations,
395
+ Row,
396
+ Params extends ParamsRecord,
397
+ Classes extends SchemaClasses = SchemaClasses
398
+ > {
399
+ readonly schema: Schema<Rels, Classes>
338
400
  readonly data: QueryData
339
401
  /** One more rule — the query's answers are the SET UNION of its rules' answers; every rule derives the same head. */
340
402
  rule<RV extends AnyRuleValue>(
341
- build: (r: QueryRuleScope<Rels>) => RV
342
- ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>>
403
+ build: (r: QueryRuleScope<Rels, Classes>) => RV
404
+ ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes>
343
405
  readonly [inferred]?: { readonly row: Row; readonly params: Params }
344
406
  }
345
407
 
@@ -360,13 +422,16 @@ type QueryRow<Q extends AnyQuery> = RowOf<Q>
360
422
  type QueryParams<Q extends AnyQuery> = ParamsOf<Q>
361
423
 
362
424
  /** The entry value of `query(S)`: the first `.rule` mints the query. */
363
- interface QueryStart<Rels extends SchemaRelations> {
364
- rule<RV extends AnyRuleValue>(build: (r: QueryRuleScope<Rels>) => RV): Query<Rels, RowOf<RV>, ParamsOf<RV>>
425
+ interface QueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> {
426
+ rule<RV extends AnyRuleValue>(
427
+ build: (r: QueryRuleScope<Rels, Classes>) => RV
428
+ ): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes>
365
429
  }
366
430
 
367
431
  /** The frozen constructor vocabulary every rule builder spreads. */
368
432
  const termOps: TermOps = Object.freeze({
369
433
  var: makeVar,
434
+ vars: makeVars,
370
435
  param: makeParam,
371
436
  inSet: makeSetParam,
372
437
  maskParam: makeMaskParam,
@@ -396,7 +461,7 @@ const termOps: TermOps = Object.freeze({
396
461
  /** One rule under construction: immutable — every chain step is a fresh state. */
397
462
  interface RuleBuildState {
398
463
  readonly items: readonly RuleItem[]
399
- readonly varFields: Readonly<Record<string, AnyField>>
464
+ readonly varFields: Readonly<Record<string, ClassedField>>
400
465
  readonly paramUses: readonly ParamUse[]
401
466
  }
402
467
 
@@ -407,56 +472,143 @@ const EMPTY_RULE: RuleBuildState = Object.freeze({
407
472
  paramUses: Object.freeze([])
408
473
  })
409
474
 
410
- /** One resolved bindings record: the atom entries, the vars it binds, and the params it uses. */
475
+ /** One resolved bindings record: the atom entries, the vars it binds (as classed slots), and the params it uses. */
411
476
  interface ResolvedBindings {
412
477
  readonly atom: AtomData
413
- readonly vars: ReadonlyArray<{ readonly name: string; readonly field: AnyField }>
478
+ readonly vars: ReadonlyArray<{ readonly name: string; readonly slot: ClassedField }>
414
479
  readonly uses: readonly ParamUse[]
415
480
  }
416
481
 
417
482
  /**
418
- * Resolves a bindings record against a relation's declared fields, in the
419
- * record's written order: terms classify by their runtime tag, everything
420
- * else is a bare literal (typed by the FIELD at lowering — the membership
421
- * typing rule included).
483
+ * The ordered matchable fields of an atom owner the runtime twin of the
484
+ * type tier's `MatchFields`: a relation's declared fields; a closed
485
+ * relation's SEALED shape, the synthetic `id` (the value's own
486
+ * roster-carrying descriptor, by identity) at ordinal 0 and the declared
487
+ * payload columns at declared index + 1 (the sealed shift, mirroring
488
+ * `spec.rs`'s resolver — a `ClosedColumn` is structurally a
489
+ * {@link RelationField}). The lowering golden pins this mapping.
490
+ */
491
+ function matchFieldsOf(owner: MatchOwner): readonly RelationField[] {
492
+ if ("axioms" in owner) {
493
+ return [Object.freeze({ name: "id", field: owner.id }), ...owner.data.columns]
494
+ }
495
+ return owner.data.fields
496
+ }
497
+
498
+ /**
499
+ * Judges one membership ARRAY at a binding position — legal exactly at a
500
+ * CLOSED-reference field (the owner ruling: ordinary u64/str membership is
501
+ * spelled through `r.inSet` params; literal arrays are the closed
502
+ * vocabulary's spelling), holding ≥ 2 DISTINCT handle names (the
503
+ * degenerate sets are refusals: empty selects nothing, one element is the
504
+ * bare literal respelled, and a duplicate member is the same respelling in
505
+ * disguise — write each member once). The returned name is
506
+ * CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
507
+ * so two spellings of one set, reordered or not, share one dense
508
+ * `ParamId`); the members are shape-checked strings here and
509
+ * roster-verified at the one verification point (`taggedHandleId`) when
510
+ * the SDK supplies the set at execute — the same moment a bound `r.inSet`
511
+ * param's members are judged.
512
+ */
513
+ function membershipSet(
514
+ context: string,
515
+ field: AnyField,
516
+ value: readonly unknown[]
517
+ ): { readonly name: string; readonly members: readonly string[] } {
518
+ if (!("closed" in field)) {
519
+ throw errors.new(
520
+ `${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`
521
+ )
522
+ }
523
+ if (value.length === 0) {
524
+ throw errors.new(`${context}: an empty membership array selects nothing — write the query you mean`)
525
+ }
526
+ if (value.length === 1) {
527
+ throw errors.new(
528
+ `${context}: a one-element membership array is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`
529
+ )
530
+ }
531
+ const seen = new Set<string>()
532
+ const members = value.map(function memberName(member) {
533
+ if (typeof member !== "string") {
534
+ throw literalShapeError(context, `a ${field.closed.name} handle name (string)`, member)
535
+ }
536
+ if (seen.has(member)) {
537
+ throw errors.new(
538
+ `${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
539
+ )
540
+ }
541
+ seen.add(member)
542
+ return member
543
+ })
544
+ const key = [...members].sort()
545
+ return { name: `∈ ${field.closed.name} ${JSON.stringify(key)}`, members: Object.freeze(members) }
546
+ }
547
+
548
+ /**
549
+ * Resolves a bindings record against an atom owner's matchable fields (a
550
+ * relation's declared fields; a closed relation's sealed id + columns), in
551
+ * the record's written order: terms classify by their runtime tag,
552
+ * everything else is a bare literal (typed by the FIELD at lowering — the
553
+ * membership typing rule included). Every bound field carries its
554
+ * law-computed class, read off the schema value's frozen class map — the
555
+ * runtime twin of the type tier's `SlotAt` lookups.
422
556
  */
423
557
  function resolveBindings(
424
558
  context: string,
425
- relation: AnyRelation,
426
- bindings: Readonly<Record<string, unknown>>
559
+ relation: MatchOwner,
560
+ bindings: Readonly<Record<string, unknown>>,
561
+ classes: SchemaClasses
427
562
  ): ResolvedBindings {
428
563
  const entries: BindingEntry[] = []
429
- const vars: Array<{ readonly name: string; readonly field: AnyField }> = []
564
+ const vars: Array<{ readonly name: string; readonly slot: ClassedField }> = []
430
565
  const uses: ParamUse[] = []
566
+ const relationClasses = classes[relation.name]
567
+ const ordered = matchFieldsOf(relation)
431
568
  for (const [fieldName, value] of Object.entries(bindings)) {
432
569
  if (value === undefined) {
433
570
  continue
434
571
  }
435
- const declared = relation.data.fields.find(function byName(candidate) {
572
+ const declared = ordered.find(function byName(candidate) {
436
573
  return candidate.name === fieldName
437
574
  })
438
575
  if (declared === undefined) {
439
576
  throw errors.new(`${context} has no field ${fieldName}`)
440
577
  }
578
+ const fieldClass = relationClasses?.[fieldName]
441
579
  let bound: BindingEntry["term"]
442
580
  if (isTerm(value)) {
443
581
  switch (value[term]) {
444
582
  case "var": {
445
583
  bound = Object.freeze({ kind: "var" as const, name: value.name })
446
- vars.push(Object.freeze({ name: value.name, field: declared.field }))
584
+ vars.push(
585
+ Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) })
586
+ )
447
587
  break
448
588
  }
449
589
  case "param": {
450
590
  bound = Object.freeze({ kind: "param" as const, name: value.name })
451
591
  uses.push(
452
- Object.freeze({ name: value.name, shape: "value" as const, anchor: declared.field, op: "binding" as const })
592
+ Object.freeze({
593
+ name: value.name,
594
+ shape: "value" as const,
595
+ anchor: declared.field,
596
+ op: "binding" as const,
597
+ members: undefined
598
+ })
453
599
  )
454
600
  break
455
601
  }
456
602
  case "setParam": {
457
603
  bound = Object.freeze({ kind: "setParam" as const, name: value.name })
458
604
  uses.push(
459
- Object.freeze({ name: value.name, shape: "set" as const, anchor: declared.field, op: "binding" as const })
605
+ Object.freeze({
606
+ name: value.name,
607
+ shape: "set" as const,
608
+ anchor: declared.field,
609
+ op: "binding" as const,
610
+ members: undefined
611
+ })
460
612
  )
461
613
  break
462
614
  }
@@ -469,10 +621,22 @@ function resolveBindings(
469
621
  `${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`
470
622
  )
471
623
  }
624
+ } else if (Array.isArray(value)) {
625
+ const set = membershipSet(`${context}.${fieldName}`, declared.field, value)
626
+ bound = Object.freeze({ kind: "literalSet" as const, name: set.name, members: set.members })
627
+ uses.push(
628
+ Object.freeze({
629
+ name: set.name,
630
+ shape: "set" as const,
631
+ anchor: declared.field,
632
+ op: "binding" as const,
633
+ members: set.members
634
+ })
635
+ )
472
636
  } else {
473
637
  bound = Object.freeze({ kind: "literal" as const, value })
474
638
  }
475
- entries.push(Object.freeze({ field: fieldName, data: declared.field, term: bound }))
639
+ entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }))
476
640
  }
477
641
  return {
478
642
  atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
@@ -484,24 +648,25 @@ function resolveBindings(
484
648
  /**
485
649
  * Extends a rule state with one positive atom. Vars bind on first
486
650
  * occurrence; every LATER occurrence (a later atom's field or a same-record
487
- * sibling) is a join and must be domain-equal — the construction-time twin
488
- * of the type tier's `JoinOk`, so the domain wall holds for untyped
489
- * callers too.
651
+ * sibling) is a join and must be class-equal — the construction-time twin
652
+ * of the type tier's `JoinOk` (bare pairs only with bare), so the domain
653
+ * wall holds for untyped callers too.
490
654
  */
491
655
  function advanceMatch(
492
656
  state: RuleBuildState,
493
- relation: AnyRelation,
494
- bindings: Readonly<Record<string, unknown>>
657
+ relation: MatchOwner,
658
+ bindings: Readonly<Record<string, unknown>>,
659
+ classes: SchemaClasses
495
660
  ): RuleBuildState {
496
- const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings)
497
- const varFields: Record<string, AnyField> = { ...state.varFields }
661
+ const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes)
662
+ const varFields: Record<string, ClassedField> = { ...state.varFields }
498
663
  for (const bound of resolved.vars) {
499
664
  const existing = varFields[bound.name]
500
665
  if (existing === undefined) {
501
- varFields[bound.name] = bound.field
502
- } else if (!fieldJoins(existing, bound.field)) {
666
+ varFields[bound.name] = bound.slot
667
+ } else if (!fieldJoins(existing, bound.slot)) {
503
668
  throw errors.new(
504
- `relation ${relation.name}: the variable ${bound.name} joins domain-unequal fields — first bound at ${renderFieldKind(existing)}, reused at ${renderFieldKind(bound.field)} (a var joins only domain-equal fields)`
669
+ `relation ${relation.name}: the variable ${bound.name} joins domain-unequal fields — first bound at ${renderFieldKind(existing)}, reused at ${renderFieldKind(bound.slot)} (a var joins only class-equal slots; bare pairs only with bare)`
505
670
  )
506
671
  }
507
672
  }
@@ -541,7 +706,7 @@ function sideUses(
541
706
  op: CmpKind,
542
707
  side: CmpTermData,
543
708
  sibling: CmpTermData,
544
- varFields: Readonly<Record<string, AnyField>>,
709
+ varFields: Readonly<Record<string, ClassedField>>,
545
710
  uses: ParamUse[]
546
711
  ): void {
547
712
  if (side.kind !== "param" && side.kind !== "setParam") {
@@ -549,7 +714,7 @@ function sideUses(
549
714
  }
550
715
  let anchor: AnyField | "measure" | undefined
551
716
  if (sibling.kind === "var") {
552
- anchor = varFields[sibling.name]
717
+ anchor = varFields[sibling.name]?.field
553
718
  } else if (sibling.kind === "measure") {
554
719
  anchor = "measure"
555
720
  } else {
@@ -560,13 +725,14 @@ function sideUses(
560
725
  name: side.name,
561
726
  shape: side.kind === "param" ? ("value" as const) : ("set" as const),
562
727
  anchor,
563
- op
728
+ op,
729
+ members: undefined
564
730
  })
565
731
  )
566
732
  }
567
733
 
568
734
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
569
- function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, AnyField>>, uses: ParamUse[]): CondData {
735
+ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedField>>, uses: ParamUse[]): CondData {
570
736
  if (cond.cond === "cmp") {
571
737
  const lhs = cmpTermDataOf(cond.op, cond.lhs)
572
738
  const rhs = cmpTermDataOf(cond.op, cond.rhs)
@@ -580,7 +746,13 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, AnyField>>
580
746
  } else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
581
747
  mask = Object.freeze({ kind: "param" as const, name: maskValue.name })
582
748
  uses.push(
583
- Object.freeze({ name: maskValue.name, shape: "mask" as const, anchor: undefined, op: "allen" as const })
749
+ Object.freeze({
750
+ name: maskValue.name,
751
+ shape: "mask" as const,
752
+ anchor: undefined,
753
+ op: "allen" as const,
754
+ members: undefined
755
+ })
584
756
  )
585
757
  } else {
586
758
  throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
@@ -602,18 +774,18 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, AnyField>>
602
774
  }
603
775
 
604
776
  /** Extends a rule state with one `.where` item (a condition or a negated atom). */
605
- function advanceWhere(state: RuleBuildState, cond: AnyCond): RuleBuildState {
777
+ function advanceWhere(state: RuleBuildState, cond: AnyCond, classes: SchemaClasses): RuleBuildState {
606
778
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
607
779
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
608
780
  }
609
781
  if (cond.cond === "not") {
610
- const relation: AnyRelation = cond.relation
782
+ const relation: MatchOwner = cond.relation
611
783
  const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
612
784
  Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
613
785
  return value !== undefined
614
786
  })
615
787
  )
616
- const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings)
788
+ const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes)
617
789
  return {
618
790
  items: Object.freeze([...state.items, Object.freeze({ kind: "negated" as const, atom: resolved.atom })]),
619
791
  varFields: state.varFields,
@@ -651,14 +823,27 @@ function isAggregateEntry(
651
823
  return typeof value === "object" && value !== null && "agg" in value
652
824
  }
653
825
 
654
- /** Classifies one select entry into its named answer column. */
826
+ /**
827
+ * Classifies one select entry into its named answer column. The `closed`
828
+ * slice is resolved LATER, at rule completion (`completeRule`), where the
829
+ * rule's `varFields` are in hand — until then every column is provisionally
830
+ * bare.
831
+ */
655
832
  function selectColumnOf(entry: unknown): SelectColumn {
656
833
  if (typeof entry === "string") {
657
- return Object.freeze({ name: entry, entry: Object.freeze({ kind: "var" as const, over: entry }) })
834
+ return Object.freeze({
835
+ name: entry,
836
+ entry: Object.freeze({ kind: "var" as const, over: entry }),
837
+ closed: undefined
838
+ })
658
839
  }
659
840
  if (isTerm(entry)) {
660
841
  if (entry[term] === "duration") {
661
- return Object.freeze({ name: entry.name, entry: Object.freeze({ kind: "measure" as const, over: entry.name }) })
842
+ return Object.freeze({
843
+ name: entry.name,
844
+ entry: Object.freeze({ kind: "measure" as const, over: entry.name }),
845
+ closed: undefined
846
+ })
662
847
  }
663
848
  throw errors.new(
664
849
  `query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`
@@ -677,7 +862,11 @@ function aggregateColumnOf(entry: {
677
862
  readonly key: unknown
678
863
  }): SelectColumn {
679
864
  function column(name: string, agg: AggData): SelectColumn {
680
- return Object.freeze({ name, entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }) })
865
+ return Object.freeze({
866
+ name,
867
+ entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }),
868
+ closed: undefined
869
+ })
681
870
  }
682
871
  const over = entry.over
683
872
  switch (entry.agg) {
@@ -718,36 +907,76 @@ function aggregateColumnOf(entry: {
718
907
  }
719
908
  }
720
909
 
910
+ /**
911
+ * The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
912
+ * § orderability): a closed reference is equality-and-membership only —
913
+ * its declaration-id order is an encoding accident, so every
914
+ * order-comparison and fold position refuses it. The construction-time
915
+ * twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
916
+ * untyped callers too (the engine cannot backstop this one: the wire IR
917
+ * carries plain u64s, no rosters).
918
+ */
919
+ function closedOrderError(context: string, position: string, vocabulary: string): Error {
920
+ return errors.new(
921
+ `${context}: ${position} is a ${vocabulary} reference — declaration order is an accident, not semantics: vocabularies do not order (docs/architecture/10-data-model.md; equality, membership, and counting remain)`
922
+ )
923
+ }
924
+
925
+ /** The comparison ops the orderable ban covers (order roster + point membership — every order-comparison position). */
926
+ function isOrderOp(op: CmpKind | "binding"): op is "lt" | "le" | "gt" | "ge" | "pointIn" {
927
+ return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn"
928
+ }
929
+
721
930
  /** Requires a var name to be bound by a relation atom of the rule. */
722
- function assertBound(context: string, varFields: Readonly<Record<string, AnyField>>, name: string): AnyField {
723
- const field = varFields[name]
724
- if (field === undefined) {
931
+ function assertBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): ClassedField {
932
+ const slot = varFields[name]
933
+ if (slot === undefined) {
725
934
  throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`)
726
935
  }
727
- return field
936
+ return slot
728
937
  }
729
938
 
730
939
  /** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
731
- function assertIntervalBound(context: string, varFields: Readonly<Record<string, AnyField>>, name: string): void {
732
- const field = assertBound(context, varFields, name)
733
- if (field.kind !== "interval") {
940
+ function assertIntervalBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): void {
941
+ const slot = assertBound(context, varFields, name)
942
+ if (slot.field.kind !== "interval") {
734
943
  throw errors.new(
735
944
  `${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`
736
945
  )
737
946
  }
738
947
  }
739
948
 
740
- /** Validates one condition's variable references against the rule's bound names. */
741
- function validateCond(context: string, varFields: Readonly<Record<string, AnyField>>, cond: CondData): void {
949
+ /**
950
+ * Validates one condition's variable references against the rule's bound
951
+ * names — and, for `eq`/`ne` over two variables, holds the class wall: the
952
+ * unification IS a join, so the two slots must be class-equal exactly as a
953
+ * match-reuse join must be (the construction-time twin of the type tier's
954
+ * `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
955
+ * this one — the query IR carries no domains — so the wall lives here for
956
+ * untyped callers too.
957
+ */
958
+ function validateCond(context: string, varFields: Readonly<Record<string, ClassedField>>, cond: CondData): void {
742
959
  if (cond.kind === "cmp") {
743
960
  for (const side of [cond.lhs, cond.rhs]) {
744
961
  if (side.kind === "var") {
745
- assertBound(context, varFields, side.name)
962
+ const slot = assertBound(context, varFields, side.name)
963
+ if (isOrderOp(cond.op) && "closed" in slot.field) {
964
+ throw closedOrderError(context, `the ${cond.op} side ${side.name}`, slot.field.closed.name)
965
+ }
746
966
  }
747
967
  if (side.kind === "measure") {
748
968
  assertIntervalBound(context, varFields, side.name)
749
969
  }
750
970
  }
971
+ if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
972
+ const lhs = assertBound(context, varFields, cond.lhs.name)
973
+ const rhs = assertBound(context, varFields, cond.rhs.name)
974
+ if (!fieldJoins(lhs, rhs)) {
975
+ throw errors.new(
976
+ `${context}: ${cond.op}(${cond.lhs.name}, ${cond.rhs.name}) unifies domain-unequal fields — ${cond.lhs.name} bound at ${renderFieldKind(lhs)}, ${cond.rhs.name} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
977
+ )
978
+ }
979
+ }
751
980
  return
752
981
  }
753
982
  for (const child of cond.children) {
@@ -756,7 +985,11 @@ function validateCond(context: string, varFields: Readonly<Record<string, AnyFie
756
985
  }
757
986
 
758
987
  /** Validates one select column's variable references. */
759
- function validateColumn(context: string, varFields: Readonly<Record<string, AnyField>>, column: SelectColumn): void {
988
+ function validateColumn(
989
+ context: string,
990
+ varFields: Readonly<Record<string, ClassedField>>,
991
+ column: SelectColumn
992
+ ): void {
760
993
  const entry = column.entry
761
994
  if (entry.kind === "var") {
762
995
  assertBound(`${context} select ${column.name}`, varFields, entry.over)
@@ -775,22 +1008,69 @@ function validateColumn(context: string, varFields: Readonly<Record<string, AnyF
775
1008
  return
776
1009
  case "fold": {
777
1010
  if (typeof agg.over === "string") {
778
- assertBound(`${context} select ${column.name}`, varFields, agg.over)
1011
+ const slot = assertBound(`${context} select ${column.name}`, varFields, agg.over)
1012
+ if ("closed" in slot.field) {
1013
+ throw closedOrderError(
1014
+ `${context} select ${column.name}`,
1015
+ `the ${agg.fold} input ${agg.over}`,
1016
+ slot.field.closed.name
1017
+ )
1018
+ }
779
1019
  return
780
1020
  }
781
1021
  assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration)
782
1022
  return
783
1023
  }
784
- case "arg":
1024
+ case "arg": {
785
1025
  assertBound(`${context} select ${column.name}`, varFields, agg.over)
786
- assertBound(`${context} select ${column.name}`, varFields, agg.key)
1026
+ const key = assertBound(`${context} select ${column.name}`, varFields, agg.key)
1027
+ if ("closed" in key.field) {
1028
+ throw closedOrderError(
1029
+ `${context} select ${column.name}`,
1030
+ `the ${agg.direction} key ${agg.key}`,
1031
+ key.field.closed.name
1032
+ )
1033
+ }
787
1034
  return
1035
+ }
788
1036
  case "pack":
789
1037
  assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over)
790
1038
  return
791
1039
  }
792
1040
  }
793
1041
 
1042
+ /**
1043
+ * Resolves the roster one select column decodes through: a projected var,
1044
+ * or an Arg-carried payload, bound at a closed-referencing field carries
1045
+ * that field's roster (read off `varFields` — the same slot the domain
1046
+ * machinery reads), and `decodeAnswers` lifts the column's row ids back to
1047
+ * handle NAMES through it — the runtime twin of the row type's `Infer`
1048
+ * claim. Every other entry decodes bare: counts are counts, the measure
1049
+ * and `pack` are never closed, and a closed FOLD is banned outright
1050
+ * ({@link closedOrderError}) before this resolution runs.
1051
+ */
1052
+ function selectClosedOf(
1053
+ varFields: Readonly<Record<string, ClassedField>>,
1054
+ entry: SelectEntryData
1055
+ ): ClosedRoster | undefined {
1056
+ let over: string | undefined
1057
+ if (entry.kind === "var") {
1058
+ over = entry.over
1059
+ } else if (entry.kind === "aggregate" && entry.agg.op === "arg") {
1060
+ over = entry.agg.over
1061
+ } else {
1062
+ over = undefined
1063
+ }
1064
+ if (over === undefined) {
1065
+ return undefined
1066
+ }
1067
+ const field = varFields[over]?.field
1068
+ if (field !== undefined && "closed" in field) {
1069
+ return field.closed
1070
+ }
1071
+ return undefined
1072
+ }
1073
+
794
1074
  /**
795
1075
  * Completes one rule: classifies the select record (written order = answer
796
1076
  * column order, names must be declaration-order-safe keys), and validates
@@ -821,9 +1101,10 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
821
1101
  `${context}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.name} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
822
1102
  )
823
1103
  }
824
- if (!fieldJoins(bound, binding.data)) {
1104
+ const negatedSlot: ClassedField = { field: binding.data, class: binding.class }
1105
+ if (!fieldJoins(bound, negatedSlot)) {
825
1106
  throw errors.new(
826
- `${context}: negated ${item.atom.relation.name} atom reuses the variable ${binding.term.name} at ${binding.field} (${renderFieldKind(binding.data)}), but the rule binds it at ${renderFieldKind(bound)} — a var joins only domain-equal fields`
1107
+ `${context}: negated ${item.atom.relation.name} atom reuses the variable ${binding.term.name} at ${binding.field} (${renderFieldKind(negatedSlot)}), but the rule binds it at ${renderFieldKind(bound)} — a var joins only class-equal slots; bare pairs only with bare`
827
1108
  )
828
1109
  }
829
1110
  }
@@ -842,10 +1123,10 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
842
1123
  if (column === undefined || column.entry.kind !== "var") {
843
1124
  return
844
1125
  }
845
- const headField = head?.varFields[column.entry.over]
846
- if (headField !== undefined && !fieldJoins(headField, bound)) {
1126
+ const headSlot = head?.varFields[column.entry.over]
1127
+ if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
847
1128
  throw errors.new(
848
- `${context}: idb ${item.rec.name} joins the variable ${name} (${renderFieldKind(bound)}) at head position ${position} (${column.name}: ${renderFieldKind(headField)}) — a var joins only domain-equal fields`
1129
+ `${context}: idb ${item.rec.name} joins the variable ${name} (${renderFieldKind(bound)}) at head position ${position} (${column.name}: ${renderFieldKind(headSlot)}) — a var joins only class-equal slots; bare pairs only with bare`
849
1130
  )
850
1131
  }
851
1132
  })
@@ -856,7 +1137,15 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
856
1137
  }
857
1138
  return Object.freeze({
858
1139
  items: state.items,
859
- select: Object.freeze([...columns]),
1140
+ select: Object.freeze(
1141
+ columns.map(function enrichColumn(column): SelectColumn {
1142
+ return Object.freeze({
1143
+ name: column.name,
1144
+ entry: column.entry,
1145
+ closed: selectClosedOf(state.varFields, column.entry)
1146
+ })
1147
+ })
1148
+ ),
860
1149
  varFields: state.varFields,
861
1150
  paramUses: state.paramUses
862
1151
  })
@@ -877,7 +1166,7 @@ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<R
877
1166
  * (bound variable names only — the creation quarantine).
878
1167
  */
879
1168
  interface RawChain {
880
- match(relation: AnyRelation, bindings: Readonly<Record<string, unknown>>): RawChain
1169
+ match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
881
1170
  where(cond: AnyCond): RawChain
882
1171
  idb(target: RecRef<string, ParamsRecord>, ...vars: readonly Var<string>[]): RawChain
883
1172
  select(...entries: readonly SelectEntry[]): RuleValue<never, never>
@@ -885,14 +1174,15 @@ interface RawChain {
885
1174
 
886
1175
  /** The runtime rule-builder shape beneath every typed scope. */
887
1176
  interface RawScope extends TermOps {
888
- match(relation: AnyRelation, bindings: Readonly<Record<string, unknown>>): RawChain
1177
+ match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
889
1178
  }
890
1179
 
891
- /** Which rule family a chain builds — gates `idb` and the recursive select. */
892
- type ChainContext =
1180
+ /** Which rule family a chain builds — gates `idb` and the recursive select — plus the schema's runtime class map (the join judge's authority). */
1181
+ type ChainContext = { readonly classes: SchemaClasses } & (
893
1182
  | { readonly kind: "query" }
894
1183
  | { readonly kind: "rec"; readonly self: RecData }
895
1184
  | { readonly kind: "output"; readonly program: ProgramState }
1185
+ )
896
1186
 
897
1187
  /** The diagnostic label of a chain context. */
898
1188
  function contextLabel(context: ChainContext): string {
@@ -948,10 +1238,10 @@ function selectColumns(context: ChainContext, entries: readonly SelectEntry[]):
948
1238
  function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
949
1239
  const chain: RawChain = {
950
1240
  match(relation, bindings) {
951
- return makeRawChain(context, advanceMatch(state, relation, bindings))
1241
+ return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes))
952
1242
  },
953
1243
  where(cond) {
954
- return makeRawChain(context, advanceWhere(state, cond))
1244
+ return makeRawChain(context, advanceWhere(state, cond, context.classes))
955
1245
  },
956
1246
  idb(target, ...vars) {
957
1247
  return makeRawChain(context, idbAdvance(context, state, target, vars))
@@ -969,7 +1259,7 @@ function makeRawScope(context: ChainContext): RawScope {
969
1259
  const scope: RawScope = {
970
1260
  ...termOps,
971
1261
  match(relation, bindings) {
972
- return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings))
1262
+ return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes))
973
1263
  }
974
1264
  }
975
1265
  Object.freeze(scope)
@@ -990,29 +1280,39 @@ function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
990
1280
  }
991
1281
 
992
1282
  /** Builds one query-rule builder (the typed face of the raw builder). */
993
- function makeQueryRuleScope<Rels extends SchemaRelations>(): QueryRuleScope<Rels> {
994
- const raw = makeRawScope({ kind: "query" })
995
- if (!isTypedScope<QueryRuleScope<Rels>>(raw)) {
1283
+ function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1284
+ classes: SchemaClasses
1285
+ ): QueryRuleScope<Rels, Classes> {
1286
+ const raw = makeRawScope({ kind: "query", classes })
1287
+ if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
996
1288
  throw errors.new("query rule builder construction incomplete")
997
1289
  }
998
1290
  return raw
999
1291
  }
1000
1292
 
1001
1293
  /** Builds one output-rule builder over a program's recs. */
1002
- function makeOutputRuleScope<Rels extends SchemaRelations>(program: ProgramState): OutputRuleScope<Rels> {
1003
- const raw = makeRawScope({ kind: "output", program })
1004
- if (!isTypedScope<OutputRuleScope<Rels>>(raw)) {
1294
+ function makeOutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1295
+ program: ProgramState
1296
+ ): OutputRuleScope<Rels, Classes> {
1297
+ const raw = makeRawScope({ kind: "output", program, classes: program.classes })
1298
+ if (!isTypedScope<OutputRuleScope<Rels, Classes>>(raw)) {
1005
1299
  throw errors.new("program output rule builder construction incomplete")
1006
1300
  }
1007
1301
  return raw
1008
1302
  }
1009
1303
 
1010
- /** One program's build-time registry: its recs in declaration order, sealed when the output is declared. */
1304
+ /** One program's build-time registry: its recs in declaration order (sealed when the output is declared) and the theory's class map. */
1011
1305
  interface ProgramState {
1012
1306
  readonly recs: RecData[]
1307
+ readonly classes: SchemaClasses
1013
1308
  sealed: boolean
1014
1309
  }
1015
1310
 
1311
+ /** Renders one head column's closed slice for the rule-alignment check's diagnostics. */
1312
+ function renderClosedSlice(closed: ClosedRoster | undefined): string {
1313
+ return closed === undefined ? "a bare value" : `a ${closed.name} reference`
1314
+ }
1315
+
1016
1316
  /** Renders one head column's signature for the rule-alignment check. */
1017
1317
  function headSignature(column: SelectColumn): string {
1018
1318
  const entry = column.entry
@@ -1029,32 +1329,91 @@ function headSignature(column: SelectColumn): string {
1029
1329
  return `${column.name}:${agg.op}`
1030
1330
  }
1031
1331
 
1332
+ /** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
1333
+ function anchorRosterOf(anchor: AnyField | "measure" | undefined): ClosedRoster | undefined {
1334
+ if (anchor === undefined || anchor === "measure") {
1335
+ return undefined
1336
+ }
1337
+ if ("closed" in anchor) {
1338
+ return anchor.closed
1339
+ }
1340
+ return undefined
1341
+ }
1342
+
1343
+ /** Renders one param anchor's closedness for the registry's coherence diagnostics. */
1344
+ function renderParamAnchor(roster: ClosedRoster | undefined): string {
1345
+ return roster === undefined ? "a non-closed position" : `a ${roster.name} reference`
1346
+ }
1347
+
1032
1348
  /**
1033
1349
  * Folds every rule's param uses (recs in declaration order first, output
1034
1350
  * rules last — exactly the lowering walk) into the query's registry:
1035
1351
  * first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
1036
- * the wire, and one name must keep one shape.
1352
+ * the wire, and one name must keep one shape AND one closedness — every
1353
+ * anchored use of one name must agree on the roster (value identity), so a
1354
+ * param anchored at a closed reference is GUARANTEED to ride the one
1355
+ * roster-verification point (`taggedHandleId`) at execute; a name anchored
1356
+ * both at a closed reference and at a non-closed position (or at two
1357
+ * vocabularies) is refused here, because the wire would translate only the
1358
+ * first anchor's reading (the type tier intersects the uses to `never`;
1359
+ * this is its runtime twin for untyped callers). A param whose anchor is a
1360
+ * CLOSED reference must never sit in an order-comparison position — the
1361
+ * anchor types its value a handle name and the engine would order the
1362
+ * translated row ids, so the pairing is refused here too (the registry is
1363
+ * the one place a name's every use and its anchoring field meet).
1037
1364
  */
1038
1365
  function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
1039
1366
  const order: string[] = []
1040
- const byName = new Map<string, { shape: ParamEntry["shape"]; anchor: ParamEntry["anchor"]; op: ParamEntry["op"] }>()
1367
+ const byName = new Map<
1368
+ string,
1369
+ {
1370
+ shape: ParamEntry["shape"]
1371
+ anchor: ParamEntry["anchor"]
1372
+ op: ParamEntry["op"]
1373
+ members: ParamEntry["members"]
1374
+ orderOp: "lt" | "le" | "gt" | "ge" | "pointIn" | undefined
1375
+ }
1376
+ >()
1041
1377
  function fold(uses: readonly ParamUse[]): void {
1042
1378
  for (const use of uses) {
1043
1379
  const existing = byName.get(use.name)
1044
1380
  if (existing === undefined) {
1045
1381
  order.push(use.name)
1046
- byName.set(use.name, { shape: use.shape, anchor: use.anchor, op: use.op })
1382
+ byName.set(use.name, {
1383
+ shape: use.shape,
1384
+ anchor: use.anchor,
1385
+ op: use.op,
1386
+ members: use.members,
1387
+ orderOp: isOrderOp(use.op) ? use.op : undefined
1388
+ })
1047
1389
  continue
1048
1390
  }
1391
+ if ((existing.members === undefined) !== (use.members === undefined)) {
1392
+ throw errors.new(
1393
+ `query param ${use.name} collides with a membership array's registry entry — name the param differently`
1394
+ )
1395
+ }
1049
1396
  if (existing.shape !== use.shape) {
1050
1397
  throw errors.new(
1051
1398
  `query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`
1052
1399
  )
1053
1400
  }
1401
+ if (existing.anchor !== undefined && use.anchor !== undefined) {
1402
+ const registered = anchorRosterOf(existing.anchor)
1403
+ const anchored = anchorRosterOf(use.anchor)
1404
+ if (registered !== anchored) {
1405
+ throw errors.new(
1406
+ `query param ${use.name} is anchored at ${renderParamAnchor(registered)} and at ${renderParamAnchor(anchored)} — a closed-anchored param translates handle names through ONE roster (one name, one domain); name the params differently`
1407
+ )
1408
+ }
1409
+ }
1054
1410
  if (existing.anchor === undefined && use.anchor !== undefined) {
1055
1411
  existing.anchor = use.anchor
1056
1412
  existing.op = use.op
1057
1413
  }
1414
+ if (existing.orderOp === undefined && isOrderOp(use.op)) {
1415
+ existing.orderOp = use.op
1416
+ }
1058
1417
  }
1059
1418
  }
1060
1419
  for (const rec of recs) {
@@ -1071,7 +1430,15 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1071
1430
  if (entry === undefined) {
1072
1431
  throw errors.new(`query param ${name} lost its registry entry`)
1073
1432
  }
1074
- return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op })
1433
+ if (
1434
+ entry.orderOp !== undefined &&
1435
+ entry.anchor !== undefined &&
1436
+ entry.anchor !== "measure" &&
1437
+ "closed" in entry.anchor
1438
+ ) {
1439
+ throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, entry.anchor.closed.name)
1440
+ }
1441
+ return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members })
1075
1442
  })
1076
1443
  )
1077
1444
  }
@@ -1102,6 +1469,19 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
1102
1469
  `every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`
1103
1470
  )
1104
1471
  }
1472
+ // The closed slice is part of the head too: one answer column decodes
1473
+ // through one roster, so a union whose rules bind a column at
1474
+ // different vocabularies (or one closed, one bare — the ids would
1475
+ // mistranslate silently) is refused pointed. Vocabulary identity is
1476
+ // value identity, the SDK's membership rule everywhere.
1477
+ rule.select.forEach(function verifyClosedSlice(column, position) {
1478
+ const lead = first.select[position]
1479
+ if (lead !== undefined && column.closed !== lead.closed) {
1480
+ throw errors.new(
1481
+ `every rule of a query derives the same head — the answer column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`
1482
+ )
1483
+ }
1484
+ })
1105
1485
  })
1106
1486
  const data: QueryData = Object.freeze({
1107
1487
  recs: Object.freeze([...recs]),
@@ -1113,7 +1493,7 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
1113
1493
  schema: theory,
1114
1494
  data,
1115
1495
  rule(build) {
1116
- const built = build(makeRawScope({ kind: "query" }))
1496
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes }))
1117
1497
  return makeRawQuery(theory, recs, [...rules, built.rule])
1118
1498
  }
1119
1499
  }
@@ -1126,21 +1506,21 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
1126
1506
  * the checkable fact — the value was assembled over the identical theory —
1127
1507
  * is verified before the raw value is admitted at its typed face.
1128
1508
  */
1129
- function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
1130
- theory: Schema<Rels>,
1509
+ function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1510
+ theory: Schema<Rels, Classes>,
1131
1511
  value: RawQuery
1132
- ): value is RawQuery & Query<Rels, Row, P> {
1512
+ ): value is RawQuery & Query<Rels, Row, P, Classes> {
1133
1513
  return value.schema === theory
1134
1514
  }
1135
1515
 
1136
1516
  /** Assembles one typed query value (rules already completed). */
1137
- function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
1138
- theory: Schema<Rels>,
1517
+ function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1518
+ theory: Schema<Rels, Classes>,
1139
1519
  recs: readonly RecData[],
1140
1520
  rules: readonly RuleData[]
1141
- ): Query<Rels, Row, P> {
1521
+ ): Query<Rels, Row, P, Classes> {
1142
1522
  const raw = makeRawQuery(theory, recs, rules)
1143
- if (!isQueryValue<Rels, Row, P>(theory, raw)) {
1523
+ if (!isQueryValue<Rels, Row, P, Classes>(theory, raw)) {
1144
1524
  throw errors.new("query value construction incomplete")
1145
1525
  }
1146
1526
  return raw
@@ -1149,13 +1529,19 @@ function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
1149
1529
  /**
1150
1530
  * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
1151
1531
  * adds one conjunctive rule; multiple rules are the set union (answers are
1152
- * SETS — no order or limit exists anywhere; hosts sort).
1532
+ * SETS — no order or limit exists anywhere; hosts sort). The schema's
1533
+ * law-computed class map rides into every rule builder — the join walls
1534
+ * compare class names off it, at the type level and at construction alike.
1153
1535
  */
1154
- function query<Rels extends SchemaRelations>(theory: Schema<Rels>): QueryStart<Rels> {
1155
- const start: QueryStart<Rels> = {
1156
- rule<RV extends AnyRuleValue>(build: (r: QueryRuleScope<Rels>) => RV): Query<Rels, RowOf<RV>, ParamsOf<RV>> {
1157
- const built = build(makeQueryRuleScope<Rels>())
1158
- return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>>(theory, [], [built.rule])
1536
+ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1537
+ theory: Schema<Rels, Classes>
1538
+ ): QueryStart<Rels, Classes> {
1539
+ const start: QueryStart<Rels, Classes> = {
1540
+ rule<RV extends AnyRuleValue>(
1541
+ build: (r: QueryRuleScope<Rels, Classes>) => RV
1542
+ ): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes> {
1543
+ const built = build(makeQueryRuleScope<Rels, Classes>(theory.classes))
1544
+ return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>, Classes>(theory, [], [built.rule])
1159
1545
  }
1160
1546
  }
1161
1547
  Object.freeze(start)
@@ -1180,25 +1566,30 @@ function isIntervalShaped(value: unknown): value is { readonly start: bigint; re
1180
1566
  }
1181
1567
 
1182
1568
  /**
1183
- * Tags one closed-reference literal: the bare handle id, verified against
1184
- * the roster (the belt the type level cannot provide — structural values
1185
- * make any bigint spellable here) and tagged u64 — queries cross ids,
1186
- * never handle names.
1569
+ * Tags one closed-reference literal: the handle NAME, verified against the
1570
+ * roster (the belt the wide fallback type cannot provide — structural
1571
+ * values make any string spellable here) and translated to its
1572
+ * declaration-order row id, tagged u64 — queries cross ids, never handle
1573
+ * names; the wire is untouched. THE single roster-verification point of
1574
+ * the query surface: atom-binding literals, comparison literals,
1575
+ * execute-time params, and membership-array members all reach it (never
1576
+ * duplicate the check per call site).
1187
1577
  */
1188
1578
  function taggedHandleId(
1189
1579
  context: string,
1190
1580
  closed: { readonly name: string; readonly handles: readonly string[] },
1191
1581
  value: unknown
1192
1582
  ): TaggedValue {
1193
- if (typeof value !== "bigint") {
1194
- throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value)
1583
+ if (typeof value !== "string") {
1584
+ throw literalShapeError(context, `a ${closed.name} handle name (string)`, value)
1195
1585
  }
1196
- if (closed.handles[Number(value)] === undefined) {
1586
+ const id = closed.handles.indexOf(value)
1587
+ if (id < 0) {
1197
1588
  throw errors.new(
1198
- `${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
1589
+ `${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
1199
1590
  )
1200
1591
  }
1201
- return { kind: "u64", value }
1592
+ return { kind: "u64", value: BigInt(id) }
1202
1593
  }
1203
1594
 
1204
1595
  /**
@@ -1348,7 +1739,13 @@ function paramIdOf(ctx: LowerContext, name: string): number {
1348
1739
  return id
1349
1740
  }
1350
1741
 
1351
- /** Lowers one EDB atom (either polarity). */
1742
+ /**
1743
+ * Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
1744
+ * same edb source — its ordinal is its record-declaration slot exactly like
1745
+ * an ordinary relation's — with field ordinals over the SEALED shape: `id`
1746
+ * at 0, each payload column at its declared index + 1 (`matchFieldsOf`
1747
+ * carries the shift; the lowering golden pins it).
1748
+ */
1352
1749
  function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1353
1750
  const member = ctx.theory.relations[atom.relation.name]
1354
1751
  if (member !== atom.relation) {
@@ -1360,8 +1757,9 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1360
1757
  if (relationId === undefined) {
1361
1758
  throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`)
1362
1759
  }
1760
+ const ordered = matchFieldsOf(atom.relation)
1363
1761
  const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
1364
- const ordinal = atom.relation.data.fields.findIndex(function byName(candidate) {
1762
+ const ordinal = ordered.findIndex(function byName(candidate) {
1365
1763
  return candidate.name === binding.field
1366
1764
  })
1367
1765
  if (ordinal < 0) {
@@ -1372,7 +1770,12 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1372
1770
  return { source: { kind: "edb", relation: relationId }, bindings }
1373
1771
  }
1374
1772
 
1375
- /** Lowers one binding term. */
1773
+ /**
1774
+ * Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
1775
+ * existing param-set term over its content-addressed registry entry — the
1776
+ * program IR is byte-identical to the same set spelled `r.inSet`; the SDK
1777
+ * supplies the translated member set itself at execute (`wireParams`).
1778
+ */
1376
1779
  function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEntry, ids: VarIds): TermIr {
1377
1780
  const bound = binding.term
1378
1781
  switch (bound.kind) {
@@ -1382,6 +1785,8 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
1382
1785
  return { kind: "param", param: paramIdOf(ctx, bound.name) }
1383
1786
  case "setParam":
1384
1787
  return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
1788
+ case "literalSet":
1789
+ return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
1385
1790
  case "literal":
1386
1791
  return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
1387
1792
  }
@@ -1436,7 +1841,7 @@ function lowerCmpTerm(
1436
1841
  /** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
1437
1842
  function cmpAnchorOf(ctx: LowerContext, rule: RuleData, sibling: CmpTermData): AnyField | "measure" | undefined {
1438
1843
  if (sibling.kind === "var") {
1439
- return rule.varFields[sibling.name]
1844
+ return rule.varFields[sibling.name]?.field
1440
1845
  }
1441
1846
  if (sibling.kind === "measure") {
1442
1847
  return "measure"