@bjornpagen/bumbledb 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/COOKBOOK.md +462 -406
  2. package/README.md +66 -31
  3. package/dist/closed.d.ts +121 -25
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +108 -42
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +12 -1
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +20 -5
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +100 -55
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js +36 -10
  14. package/dist/face.js.map +1 -1
  15. package/dist/fields.d.ts +50 -79
  16. package/dist/fields.d.ts.map +1 -1
  17. package/dist/fields.js +20 -53
  18. package/dist/fields.js.map +1 -1
  19. package/dist/index.d.ts +17 -12
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -6
  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/native.d.ts +6 -2
  32. package/dist/native.d.ts.map +1 -1
  33. package/dist/native.js.map +1 -1
  34. package/dist/query/atom.d.ts +88 -50
  35. package/dist/query/atom.d.ts.map +1 -1
  36. package/dist/query/atom.js +4 -1
  37. package/dist/query/atom.js.map +1 -1
  38. package/dist/query/lower.d.ts +71 -56
  39. package/dist/query/lower.d.ts.map +1 -1
  40. package/dist/query/lower.js +90 -43
  41. package/dist/query/lower.js.map +1 -1
  42. package/dist/query/predicate.d.ts +10 -9
  43. package/dist/query/predicate.d.ts.map +1 -1
  44. package/dist/query/predicate.js +2 -2
  45. package/dist/query/predicate.js.map +1 -1
  46. package/dist/query/scope.d.ts +76 -41
  47. package/dist/query/scope.d.ts.map +1 -1
  48. package/dist/query/scope.js +77 -30
  49. package/dist/query/scope.js.map +1 -1
  50. package/dist/query/select.d.ts +5 -5
  51. package/dist/query/select.d.ts.map +1 -1
  52. package/dist/relation.d.ts +21 -8
  53. package/dist/relation.d.ts.map +1 -1
  54. package/dist/relation.js +13 -7
  55. package/dist/relation.js.map +1 -1
  56. package/dist/schema.d.ts +41 -3
  57. package/dist/schema.d.ts.map +1 -1
  58. package/dist/schema.js +16 -2
  59. package/dist/schema.js.map +1 -1
  60. package/dist/spec.d.ts +7 -6
  61. package/dist/spec.d.ts.map +1 -1
  62. package/dist/spec.js.map +1 -1
  63. package/dist/statements.d.ts +61 -31
  64. package/dist/statements.d.ts.map +1 -1
  65. package/dist/statements.js +22 -17
  66. package/dist/statements.js.map +1 -1
  67. package/package.json +2 -2
  68. package/src/closed.ts +243 -68
  69. package/src/db.ts +23 -5
  70. package/src/face.ts +162 -84
  71. package/src/fields.ts +57 -136
  72. package/src/index.ts +42 -16
  73. package/src/law.ts +519 -0
  74. package/src/lower.ts +36 -23
  75. package/src/native.ts +6 -2
  76. package/src/query/atom.ts +105 -58
  77. package/src/query/lower.ts +271 -139
  78. package/src/query/predicate.ts +43 -33
  79. package/src/query/scope.ts +125 -49
  80. package/src/query/select.ts +5 -5
  81. package/src/relation.ts +15 -9
  82. package/src/schema.ts +48 -7
  83. package/src/spec.ts +7 -6
  84. package/src/statements.ts +83 -43
@@ -28,6 +28,7 @@
28
28
  import * as errors from "@superbuilders/errors"
29
29
  import type { AnyField } 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,43 +472,68 @@ 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
+ * Resolves a bindings record against an atom owner's matchable fields (a
500
+ * relation's declared fields; a closed relation's sealed id + columns), in
501
+ * the record's written order: terms classify by their runtime tag,
502
+ * everything else is a bare literal (typed by the FIELD at lowering — the
503
+ * membership typing rule included). Every bound field carries its
504
+ * law-computed class, read off the schema value's frozen class map — the
505
+ * runtime twin of the type tier's `SlotAt` lookups.
422
506
  */
423
507
  function resolveBindings(
424
508
  context: string,
425
- relation: AnyRelation,
426
- bindings: Readonly<Record<string, unknown>>
509
+ relation: MatchOwner,
510
+ bindings: Readonly<Record<string, unknown>>,
511
+ classes: SchemaClasses
427
512
  ): ResolvedBindings {
428
513
  const entries: BindingEntry[] = []
429
- const vars: Array<{ readonly name: string; readonly field: AnyField }> = []
514
+ const vars: Array<{ readonly name: string; readonly slot: ClassedField }> = []
430
515
  const uses: ParamUse[] = []
516
+ const relationClasses = classes[relation.name]
517
+ const ordered = matchFieldsOf(relation)
431
518
  for (const [fieldName, value] of Object.entries(bindings)) {
432
519
  if (value === undefined) {
433
520
  continue
434
521
  }
435
- const declared = relation.data.fields.find(function byName(candidate) {
522
+ const declared = ordered.find(function byName(candidate) {
436
523
  return candidate.name === fieldName
437
524
  })
438
525
  if (declared === undefined) {
439
526
  throw errors.new(`${context} has no field ${fieldName}`)
440
527
  }
528
+ const fieldClass = relationClasses?.[fieldName]
441
529
  let bound: BindingEntry["term"]
442
530
  if (isTerm(value)) {
443
531
  switch (value[term]) {
444
532
  case "var": {
445
533
  bound = Object.freeze({ kind: "var" as const, name: value.name })
446
- vars.push(Object.freeze({ name: value.name, field: declared.field }))
534
+ vars.push(
535
+ Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) })
536
+ )
447
537
  break
448
538
  }
449
539
  case "param": {
@@ -472,7 +562,7 @@ function resolveBindings(
472
562
  } else {
473
563
  bound = Object.freeze({ kind: "literal" as const, value })
474
564
  }
475
- entries.push(Object.freeze({ field: fieldName, data: declared.field, term: bound }))
565
+ entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }))
476
566
  }
477
567
  return {
478
568
  atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
@@ -484,24 +574,25 @@ function resolveBindings(
484
574
  /**
485
575
  * Extends a rule state with one positive atom. Vars bind on first
486
576
  * 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.
577
+ * sibling) is a join and must be class-equal — the construction-time twin
578
+ * of the type tier's `JoinOk` (bare pairs only with bare), so the domain
579
+ * wall holds for untyped callers too.
490
580
  */
491
581
  function advanceMatch(
492
582
  state: RuleBuildState,
493
- relation: AnyRelation,
494
- bindings: Readonly<Record<string, unknown>>
583
+ relation: MatchOwner,
584
+ bindings: Readonly<Record<string, unknown>>,
585
+ classes: SchemaClasses
495
586
  ): RuleBuildState {
496
- const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings)
497
- const varFields: Record<string, AnyField> = { ...state.varFields }
587
+ const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes)
588
+ const varFields: Record<string, ClassedField> = { ...state.varFields }
498
589
  for (const bound of resolved.vars) {
499
590
  const existing = varFields[bound.name]
500
591
  if (existing === undefined) {
501
- varFields[bound.name] = bound.field
502
- } else if (!fieldJoins(existing, bound.field)) {
592
+ varFields[bound.name] = bound.slot
593
+ } else if (!fieldJoins(existing, bound.slot)) {
503
594
  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)`
595
+ `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
596
  )
506
597
  }
507
598
  }
@@ -541,7 +632,7 @@ function sideUses(
541
632
  op: CmpKind,
542
633
  side: CmpTermData,
543
634
  sibling: CmpTermData,
544
- varFields: Readonly<Record<string, AnyField>>,
635
+ varFields: Readonly<Record<string, ClassedField>>,
545
636
  uses: ParamUse[]
546
637
  ): void {
547
638
  if (side.kind !== "param" && side.kind !== "setParam") {
@@ -549,7 +640,7 @@ function sideUses(
549
640
  }
550
641
  let anchor: AnyField | "measure" | undefined
551
642
  if (sibling.kind === "var") {
552
- anchor = varFields[sibling.name]
643
+ anchor = varFields[sibling.name]?.field
553
644
  } else if (sibling.kind === "measure") {
554
645
  anchor = "measure"
555
646
  } else {
@@ -566,7 +657,7 @@ function sideUses(
566
657
  }
567
658
 
568
659
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
569
- function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, AnyField>>, uses: ParamUse[]): CondData {
660
+ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedField>>, uses: ParamUse[]): CondData {
570
661
  if (cond.cond === "cmp") {
571
662
  const lhs = cmpTermDataOf(cond.op, cond.lhs)
572
663
  const rhs = cmpTermDataOf(cond.op, cond.rhs)
@@ -602,18 +693,18 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, AnyField>>
602
693
  }
603
694
 
604
695
  /** Extends a rule state with one `.where` item (a condition or a negated atom). */
605
- function advanceWhere(state: RuleBuildState, cond: AnyCond): RuleBuildState {
696
+ function advanceWhere(state: RuleBuildState, cond: AnyCond, classes: SchemaClasses): RuleBuildState {
606
697
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
607
698
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
608
699
  }
609
700
  if (cond.cond === "not") {
610
- const relation: AnyRelation = cond.relation
701
+ const relation: MatchOwner = cond.relation
611
702
  const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
612
703
  Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
613
704
  return value !== undefined
614
705
  })
615
706
  )
616
- const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings)
707
+ const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes)
617
708
  return {
618
709
  items: Object.freeze([...state.items, Object.freeze({ kind: "negated" as const, atom: resolved.atom })]),
619
710
  varFields: state.varFields,
@@ -719,26 +810,34 @@ function aggregateColumnOf(entry: {
719
810
  }
720
811
 
721
812
  /** 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) {
813
+ function assertBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): ClassedField {
814
+ const slot = varFields[name]
815
+ if (slot === undefined) {
725
816
  throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`)
726
817
  }
727
- return field
818
+ return slot
728
819
  }
729
820
 
730
821
  /** 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") {
822
+ function assertIntervalBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): void {
823
+ const slot = assertBound(context, varFields, name)
824
+ if (slot.field.kind !== "interval") {
734
825
  throw errors.new(
735
826
  `${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`
736
827
  )
737
828
  }
738
829
  }
739
830
 
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 {
831
+ /**
832
+ * Validates one condition's variable references against the rule's bound
833
+ * names — and, for `eq`/`ne` over two variables, holds the class wall: the
834
+ * unification IS a join, so the two slots must be class-equal exactly as a
835
+ * match-reuse join must be (the construction-time twin of the type tier's
836
+ * `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
837
+ * this one — the query IR carries no domains — so the wall lives here for
838
+ * untyped callers too.
839
+ */
840
+ function validateCond(context: string, varFields: Readonly<Record<string, ClassedField>>, cond: CondData): void {
742
841
  if (cond.kind === "cmp") {
743
842
  for (const side of [cond.lhs, cond.rhs]) {
744
843
  if (side.kind === "var") {
@@ -748,6 +847,15 @@ function validateCond(context: string, varFields: Readonly<Record<string, AnyFie
748
847
  assertIntervalBound(context, varFields, side.name)
749
848
  }
750
849
  }
850
+ if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
851
+ const lhs = assertBound(context, varFields, cond.lhs.name)
852
+ const rhs = assertBound(context, varFields, cond.rhs.name)
853
+ if (!fieldJoins(lhs, rhs)) {
854
+ throw errors.new(
855
+ `${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)`
856
+ )
857
+ }
858
+ }
751
859
  return
752
860
  }
753
861
  for (const child of cond.children) {
@@ -756,7 +864,11 @@ function validateCond(context: string, varFields: Readonly<Record<string, AnyFie
756
864
  }
757
865
 
758
866
  /** Validates one select column's variable references. */
759
- function validateColumn(context: string, varFields: Readonly<Record<string, AnyField>>, column: SelectColumn): void {
867
+ function validateColumn(
868
+ context: string,
869
+ varFields: Readonly<Record<string, ClassedField>>,
870
+ column: SelectColumn
871
+ ): void {
760
872
  const entry = column.entry
761
873
  if (entry.kind === "var") {
762
874
  assertBound(`${context} select ${column.name}`, varFields, entry.over)
@@ -821,9 +933,10 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
821
933
  `${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
934
  )
823
935
  }
824
- if (!fieldJoins(bound, binding.data)) {
936
+ const negatedSlot: ClassedField = { field: binding.data, class: binding.class }
937
+ if (!fieldJoins(bound, negatedSlot)) {
825
938
  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`
939
+ `${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
940
  )
828
941
  }
829
942
  }
@@ -842,10 +955,10 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
842
955
  if (column === undefined || column.entry.kind !== "var") {
843
956
  return
844
957
  }
845
- const headField = head?.varFields[column.entry.over]
846
- if (headField !== undefined && !fieldJoins(headField, bound)) {
958
+ const headSlot = head?.varFields[column.entry.over]
959
+ if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
847
960
  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`
961
+ `${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
962
  )
850
963
  }
851
964
  })
@@ -877,7 +990,7 @@ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<R
877
990
  * (bound variable names only — the creation quarantine).
878
991
  */
879
992
  interface RawChain {
880
- match(relation: AnyRelation, bindings: Readonly<Record<string, unknown>>): RawChain
993
+ match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
881
994
  where(cond: AnyCond): RawChain
882
995
  idb(target: RecRef<string, ParamsRecord>, ...vars: readonly Var<string>[]): RawChain
883
996
  select(...entries: readonly SelectEntry[]): RuleValue<never, never>
@@ -885,14 +998,15 @@ interface RawChain {
885
998
 
886
999
  /** The runtime rule-builder shape beneath every typed scope. */
887
1000
  interface RawScope extends TermOps {
888
- match(relation: AnyRelation, bindings: Readonly<Record<string, unknown>>): RawChain
1001
+ match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
889
1002
  }
890
1003
 
891
- /** Which rule family a chain builds — gates `idb` and the recursive select. */
892
- type ChainContext =
1004
+ /** Which rule family a chain builds — gates `idb` and the recursive select — plus the schema's runtime class map (the join judge's authority). */
1005
+ type ChainContext = { readonly classes: SchemaClasses } & (
893
1006
  | { readonly kind: "query" }
894
1007
  | { readonly kind: "rec"; readonly self: RecData }
895
1008
  | { readonly kind: "output"; readonly program: ProgramState }
1009
+ )
896
1010
 
897
1011
  /** The diagnostic label of a chain context. */
898
1012
  function contextLabel(context: ChainContext): string {
@@ -948,10 +1062,10 @@ function selectColumns(context: ChainContext, entries: readonly SelectEntry[]):
948
1062
  function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
949
1063
  const chain: RawChain = {
950
1064
  match(relation, bindings) {
951
- return makeRawChain(context, advanceMatch(state, relation, bindings))
1065
+ return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes))
952
1066
  },
953
1067
  where(cond) {
954
- return makeRawChain(context, advanceWhere(state, cond))
1068
+ return makeRawChain(context, advanceWhere(state, cond, context.classes))
955
1069
  },
956
1070
  idb(target, ...vars) {
957
1071
  return makeRawChain(context, idbAdvance(context, state, target, vars))
@@ -969,7 +1083,7 @@ function makeRawScope(context: ChainContext): RawScope {
969
1083
  const scope: RawScope = {
970
1084
  ...termOps,
971
1085
  match(relation, bindings) {
972
- return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings))
1086
+ return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes))
973
1087
  }
974
1088
  }
975
1089
  Object.freeze(scope)
@@ -990,26 +1104,31 @@ function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
990
1104
  }
991
1105
 
992
1106
  /** 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)) {
1107
+ function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1108
+ classes: SchemaClasses
1109
+ ): QueryRuleScope<Rels, Classes> {
1110
+ const raw = makeRawScope({ kind: "query", classes })
1111
+ if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
996
1112
  throw errors.new("query rule builder construction incomplete")
997
1113
  }
998
1114
  return raw
999
1115
  }
1000
1116
 
1001
1117
  /** 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)) {
1118
+ function makeOutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1119
+ program: ProgramState
1120
+ ): OutputRuleScope<Rels, Classes> {
1121
+ const raw = makeRawScope({ kind: "output", program, classes: program.classes })
1122
+ if (!isTypedScope<OutputRuleScope<Rels, Classes>>(raw)) {
1005
1123
  throw errors.new("program output rule builder construction incomplete")
1006
1124
  }
1007
1125
  return raw
1008
1126
  }
1009
1127
 
1010
- /** One program's build-time registry: its recs in declaration order, sealed when the output is declared. */
1128
+ /** One program's build-time registry: its recs in declaration order (sealed when the output is declared) and the theory's class map. */
1011
1129
  interface ProgramState {
1012
1130
  readonly recs: RecData[]
1131
+ readonly classes: SchemaClasses
1013
1132
  sealed: boolean
1014
1133
  }
1015
1134
 
@@ -1113,7 +1232,7 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
1113
1232
  schema: theory,
1114
1233
  data,
1115
1234
  rule(build) {
1116
- const built = build(makeRawScope({ kind: "query" }))
1235
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes }))
1117
1236
  return makeRawQuery(theory, recs, [...rules, built.rule])
1118
1237
  }
1119
1238
  }
@@ -1126,21 +1245,21 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
1126
1245
  * the checkable fact — the value was assembled over the identical theory —
1127
1246
  * is verified before the raw value is admitted at its typed face.
1128
1247
  */
1129
- function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
1130
- theory: Schema<Rels>,
1248
+ function isQueryValue<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1249
+ theory: Schema<Rels, Classes>,
1131
1250
  value: RawQuery
1132
- ): value is RawQuery & Query<Rels, Row, P> {
1251
+ ): value is RawQuery & Query<Rels, Row, P, Classes> {
1133
1252
  return value.schema === theory
1134
1253
  }
1135
1254
 
1136
1255
  /** Assembles one typed query value (rules already completed). */
1137
- function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
1138
- theory: Schema<Rels>,
1256
+ function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Classes extends SchemaClasses>(
1257
+ theory: Schema<Rels, Classes>,
1139
1258
  recs: readonly RecData[],
1140
1259
  rules: readonly RuleData[]
1141
- ): Query<Rels, Row, P> {
1260
+ ): Query<Rels, Row, P, Classes> {
1142
1261
  const raw = makeRawQuery(theory, recs, rules)
1143
- if (!isQueryValue<Rels, Row, P>(theory, raw)) {
1262
+ if (!isQueryValue<Rels, Row, P, Classes>(theory, raw)) {
1144
1263
  throw errors.new("query value construction incomplete")
1145
1264
  }
1146
1265
  return raw
@@ -1149,13 +1268,19 @@ function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord>(
1149
1268
  /**
1150
1269
  * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
1151
1270
  * adds one conjunctive rule; multiple rules are the set union (answers are
1152
- * SETS — no order or limit exists anywhere; hosts sort).
1271
+ * SETS — no order or limit exists anywhere; hosts sort). The schema's
1272
+ * law-computed class map rides into every rule builder — the join walls
1273
+ * compare class names off it, at the type level and at construction alike.
1153
1274
  */
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])
1275
+ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1276
+ theory: Schema<Rels, Classes>
1277
+ ): QueryStart<Rels, Classes> {
1278
+ const start: QueryStart<Rels, Classes> = {
1279
+ rule<RV extends AnyRuleValue>(
1280
+ build: (r: QueryRuleScope<Rels, Classes>) => RV
1281
+ ): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes> {
1282
+ const built = build(makeQueryRuleScope<Rels, Classes>(theory.classes))
1283
+ return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>, Classes>(theory, [], [built.rule])
1159
1284
  }
1160
1285
  }
1161
1286
  Object.freeze(start)
@@ -1348,7 +1473,13 @@ function paramIdOf(ctx: LowerContext, name: string): number {
1348
1473
  return id
1349
1474
  }
1350
1475
 
1351
- /** Lowers one EDB atom (either polarity). */
1476
+ /**
1477
+ * Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
1478
+ * same edb source — its ordinal is its record-declaration slot exactly like
1479
+ * an ordinary relation's — with field ordinals over the SEALED shape: `id`
1480
+ * at 0, each payload column at its declared index + 1 (`matchFieldsOf`
1481
+ * carries the shift; the lowering golden pins it).
1482
+ */
1352
1483
  function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1353
1484
  const member = ctx.theory.relations[atom.relation.name]
1354
1485
  if (member !== atom.relation) {
@@ -1360,8 +1491,9 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1360
1491
  if (relationId === undefined) {
1361
1492
  throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`)
1362
1493
  }
1494
+ const ordered = matchFieldsOf(atom.relation)
1363
1495
  const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
1364
- const ordinal = atom.relation.data.fields.findIndex(function byName(candidate) {
1496
+ const ordinal = ordered.findIndex(function byName(candidate) {
1365
1497
  return candidate.name === binding.field
1366
1498
  })
1367
1499
  if (ordinal < 0) {
@@ -1436,7 +1568,7 @@ function lowerCmpTerm(
1436
1568
  /** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
1437
1569
  function cmpAnchorOf(ctx: LowerContext, rule: RuleData, sibling: CmpTermData): AnyField | "measure" | undefined {
1438
1570
  if (sibling.kind === "var") {
1439
- return rule.varFields[sibling.name]
1571
+ return rule.varFields[sibling.name]?.field
1440
1572
  }
1441
1573
  if (sibling.kind === "measure") {
1442
1574
  return "measure"