@bjornpagen/bumbledb 0.4.0 → 0.6.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 (103) hide show
  1. package/COOKBOOK.md +191 -92
  2. package/README.md +9 -7
  3. package/dist/closed.d.ts +30 -2
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +66 -20
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +33 -6
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +89 -72
  10. package/dist/db.js.map +1 -1
  11. package/dist/exhume.d.ts.map +1 -1
  12. package/dist/exhume.js +1 -14
  13. package/dist/exhume.js.map +1 -1
  14. package/dist/face.d.ts +1 -1
  15. package/dist/face.d.ts.map +1 -1
  16. package/dist/face.js +2 -1
  17. package/dist/face.js.map +1 -1
  18. package/dist/fields.d.ts +18 -1
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +44 -16
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +18 -10
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +13 -6
  25. package/dist/index.js.map +1 -1
  26. package/dist/law.d.ts +2 -1
  27. package/dist/law.d.ts.map +1 -1
  28. package/dist/law.js +15 -14
  29. package/dist/law.js.map +1 -1
  30. package/dist/lower.d.ts.map +1 -1
  31. package/dist/lower.js +1 -7
  32. package/dist/lower.js.map +1 -1
  33. package/dist/marshal.d.ts.map +1 -1
  34. package/dist/marshal.js +15 -27
  35. package/dist/marshal.js.map +1 -1
  36. package/dist/native.d.ts +21 -2
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/native.js +20 -3
  39. package/dist/native.js.map +1 -1
  40. package/dist/order.d.ts +36 -0
  41. package/dist/order.d.ts.map +1 -0
  42. package/dist/order.js +135 -0
  43. package/dist/order.js.map +1 -0
  44. package/dist/query/atom.d.ts +140 -206
  45. package/dist/query/atom.d.ts.map +1 -1
  46. package/dist/query/atom.js +33 -52
  47. package/dist/query/atom.js.map +1 -1
  48. package/dist/query/find.d.ts +116 -0
  49. package/dist/query/find.d.ts.map +1 -0
  50. package/dist/query/{select.js → find.js} +22 -22
  51. package/dist/query/find.js.map +1 -0
  52. package/dist/query/lower.d.ts +124 -162
  53. package/dist/query/lower.d.ts.map +1 -1
  54. package/dist/query/lower.js +462 -507
  55. package/dist/query/lower.js.map +1 -1
  56. package/dist/query/predicate.d.ts +22 -14
  57. package/dist/query/predicate.d.ts.map +1 -1
  58. package/dist/query/predicate.js +45 -20
  59. package/dist/query/predicate.js.map +1 -1
  60. package/dist/query/run.d.ts +3 -3
  61. package/dist/query/run.d.ts.map +1 -1
  62. package/dist/query/run.js +10 -11
  63. package/dist/query/run.js.map +1 -1
  64. package/dist/query/scope.d.ts +124 -83
  65. package/dist/query/scope.d.ts.map +1 -1
  66. package/dist/query/scope.js +77 -70
  67. package/dist/query/scope.js.map +1 -1
  68. package/dist/relation.d.ts +9 -22
  69. package/dist/relation.d.ts.map +1 -1
  70. package/dist/relation.js +9 -28
  71. package/dist/relation.js.map +1 -1
  72. package/dist/schema.d.ts.map +1 -1
  73. package/dist/schema.js +7 -31
  74. package/dist/schema.js.map +1 -1
  75. package/dist/statements.d.ts +5 -2
  76. package/dist/statements.d.ts.map +1 -1
  77. package/dist/statements.js +30 -35
  78. package/dist/statements.js.map +1 -1
  79. package/package.json +2 -5
  80. package/src/closed.ts +73 -28
  81. package/src/db.ts +126 -88
  82. package/src/exhume.ts +1 -15
  83. package/src/face.ts +4 -3
  84. package/src/fields.ts +58 -16
  85. package/src/index.ts +19 -13
  86. package/src/law.ts +15 -14
  87. package/src/lower.ts +2 -9
  88. package/src/marshal.ts +15 -31
  89. package/src/native.ts +22 -4
  90. package/src/order.ts +156 -0
  91. package/src/query/atom.ts +186 -276
  92. package/src/query/find.ts +212 -0
  93. package/src/query/lower.ts +614 -749
  94. package/src/query/predicate.ts +47 -20
  95. package/src/query/run.ts +11 -13
  96. package/src/query/scope.ts +165 -132
  97. package/src/relation.ts +9 -51
  98. package/src/schema.ts +7 -33
  99. package/src/statements.ts +33 -38
  100. package/dist/query/select.d.ts +0 -128
  101. package/dist/query/select.d.ts.map +0 -1
  102. package/dist/query/select.js.map +0 -1
  103. package/src/query/select.ts +0 -215
@@ -1,33 +1,43 @@
1
1
  /**
2
- * `query()` and the IR lowering, STRUCTURAL edition. A query is built
3
- * kysely-shaped — `query(S).rule(r => r.match(Rel, { f: r.var("x") })
4
- * .where(r.eq(r.var("x"), r.param("p"))).select("x"))` and is an INERT
5
- * value: `Query<Rels, Row, Params>` with `Row` inferred from each rule's
6
- * `.select` and `Params` inferred to be EXACTLY the params the rules use
7
- * (params are typed BY USE; a param value no rule uses never registers, so
8
- * every query executes under its own inferred type). Vars are string
9
- * names, domain-typed by the field they first bind and joined by reuse —
10
- * the rule builder's environment carries name → field descriptor through
11
- * the chain, checked structurally at every reuse (`JoinOk`), so the old
12
- * brand-equal join is now the domain-equal compile error. Lowering is a
13
- * pure function of the query value down to the bridge's `ProgramIr`
14
- * (`bumbledb/crates/bumbledb/src/ir.rs`, the bijection target): relations
15
- * by declaration ordinal (the declaration-order-is-ids law the engine's
16
- * manifest pins), variables by dense per-rule first-occurrence ids
17
- * (rule-scoped, exactly as the IR scopes them), params by first-use order
18
- * across the program walk. Lowering is STABLE the same query value
19
- * lowers to deeply-equal IR every time, and two identically-written
20
- * queries lower identically. Construction validates negation safety and
21
- * name-boundness (typed, naming the variable earlier and warmer than
22
- * the engine's refusal); everything else (strata, types, aggregate
23
- * rosters, rule caps) is the ENGINE's judge, surfacing its typed errors
24
- * at prepare. No invented limits: rule and predicate counts are never
25
- * pre-checked here.
2
+ * `query()` and the IR lowering, REFERENCE-IDENTITY edition. A query is
3
+ * built kysely-shaped — variables minted by {@link v} outside the rule and
4
+ * reused by REFERENCE to join:
5
+ *
6
+ * query(S).rule((r) => {
7
+ * const acct = v(Account)
8
+ * const h = v(Holder)
9
+ * return r
10
+ * .match(Account, { id: acct.id, holder: acct.holder })
11
+ * .match(Holder, { id: acct.holder })
12
+ * .where(r.eq(acct.holder, r.param("root")))
13
+ * .find({ account: acct.id, holder: acct.holder })
14
+ * })
15
+ *
16
+ * and is an INERT value: `Query<Rels, Row, Params>` with `Row` inferred
17
+ * from each rule's `.find` RECORD (its keys ARE the answer columns) and
18
+ * `Params` inferred to be EXACTLY the params the rules use (params are typed
19
+ * BY USE; a param no rule uses never registers). Variable IDENTITY is the
20
+ * object reference: reusing one value across binding positions IS the join,
21
+ * and a name-collision join is unrepresentable. Each binding position is
22
+ * judged against the variable's MINT slot and because {@link JoinOk} is an
23
+ * equality, that alone makes every cross-binding join transitively
24
+ * class-equal. Lowering is a pure function of the query value down to the
25
+ * bridge's `ProgramIr` (`bumbledb/crates/bumbledb/src/ir.rs`): relations by
26
+ * declaration ordinal, variables by dense per-rule first-occurrence ids
27
+ * (keyed on the object REFERENCE — the discipline is unchanged, only the map
28
+ * key moved from name to reference), params by first-use order. Lowering is
29
+ * STABLE — the same query value lowers to deeply-equal IR every time, and
30
+ * two identically-written queries (fresh mints each) lower identically.
31
+ * Construction validates negation safety and boundness (typed by the var's
32
+ * label — object identity is invisible to the type tier, so these are
33
+ * construction-time walls); everything else (strata, types, aggregate
34
+ * rosters, rule caps) is the ENGINE's judge, surfacing at prepare.
26
35
  */
27
36
 
28
37
  import * as errors from "@superbuilders/errors"
38
+ import { sealedFieldsOf } from "#closed.ts"
29
39
  import type { AnyField, ClosedRoster } from "#fields.ts"
30
- import { assertDeclarationOrderKey } from "#fields.ts"
40
+ import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
31
41
  import type { ClassRecordOf, SchemaClasses } from "#law.ts"
32
42
  import type {
33
43
  AtomIr,
@@ -46,7 +56,6 @@ import type {
46
56
  AggData,
47
57
  AnyCond,
48
58
  AtomData,
49
- BindEnv,
50
59
  BindingEntry,
51
60
  BindParamsShape,
52
61
  CheckBindings,
@@ -56,6 +65,8 @@ import type {
56
65
  CmpTermData,
57
66
  CondData,
58
67
  CondParamsShape,
68
+ FindColumn,
69
+ FindEntryData,
59
70
  MaskData,
60
71
  MatchFields,
61
72
  MatchOwner,
@@ -63,21 +74,20 @@ import type {
63
74
  ParamUse,
64
75
  RecData,
65
76
  RuleData,
66
- RuleItem,
67
- SelectColumn,
68
- SelectEntryData,
69
- TreeData
77
+ RuleItem
70
78
  } from "#query/atom.ts"
71
- import { allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
79
+ import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
80
+ import type { CheckFind, CheckRecFind, FindShape, HeadRecordOf, RowOfFind } from "#query/find.ts"
81
+ import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/find.ts"
72
82
  import type {
83
+ AnyVar,
73
84
  ClassedField,
74
- EnvShape,
75
85
  Flatten,
76
86
  InferredOf,
77
87
  JoinOk,
88
+ MintSlotOf,
78
89
  ParamEntry,
79
- ParamsRecord,
80
- Var
90
+ ParamsRecord
81
91
  } from "#query/scope.ts"
82
92
  import {
83
93
  fieldJoins,
@@ -87,29 +97,18 @@ import {
87
97
  makeMaskParam,
88
98
  makeParam,
89
99
  makeSetParam,
90
- makeVar,
91
- makeVars,
92
100
  renderFieldKind,
93
101
  term
94
102
  } from "#query/scope.ts"
95
- import type { CheckNameSelect, CheckSelect, RowOfNameSelect, RowOfSelect, SelectEntry } from "#query/select.ts"
96
- import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts"
97
- import type { FieldsShape, RelationField } from "#relation.ts"
98
103
  import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
99
104
 
100
105
  /**
101
106
  * 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).
107
+ * closed vocabularies (ψ query atoms; the ENGINE decides folding vs virtual
108
+ * image, the SDK lowers pass-through).
106
109
  */
107
110
  type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], MatchOwner>
108
111
 
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
112
-
113
112
  /** Reads an inferred-params carrier off a rec reference or rule value. */
114
113
  type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends ParamsRecord } ? P : Record<never, never>
115
114
 
@@ -117,19 +116,16 @@ type ParamsOf<T> = InferredOf<T> extends { readonly params: infer P extends Para
117
116
  type RowOf<T> = InferredOf<T> extends { readonly row: infer R } ? R : never
118
117
 
119
118
  /**
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).
119
+ * A recursive predicate's HEAD signature as classed slots, keyed by column
120
+ * name — carried on the rec reference so an `idb` join can be judged against
121
+ * it; `undefined` on values that carry no head.
125
122
  */
126
- type HeadShape = readonly ClassedField[] | undefined
123
+ type HeadShape = Readonly<Record<string, ClassedField>> | undefined
127
124
 
128
125
  /**
129
126
  * One finished rule as a plain value: the runtime data plus the inferred
130
- * row/params carrier (and, for a RECURSIVE rule, the head's positional
131
- * field descriptors — the signature `idb` joins pair against).
132
- * `.rule(...)` consumes it; hosts never build one by hand.
127
+ * row/params carrier (and, for a RECURSIVE rule, the head record of classed
128
+ * slots `idb` pairs against). `.rule(...)` consumes it.
133
129
  */
134
130
  interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefined> {
135
131
  readonly rule: RuleData
@@ -139,22 +135,14 @@ interface RuleValue<Row, P extends ParamsRecord, Head extends HeadShape = undefi
139
135
  /** Any finished rule value. */
140
136
  type AnyRuleValue = RuleValue<unknown, ParamsRecord, HeadShape>
141
137
 
142
- /** The positional head-field tuple of a recursive rule's names-only select. */
143
- type HeadFieldsOf<Env extends EnvShape, S extends readonly string[]> = {
144
- readonly [I in keyof S]: Env[S[I] & keyof Env]
145
- }
146
-
147
138
  /** Reads an inferred-head carrier off a rule value or rec reference. */
148
- type HeadOf<T> = InferredOf<T> extends { readonly head: infer H extends readonly ClassedField[] } ? H : undefined
139
+ type HeadOf<T> =
140
+ InferredOf<T> extends { readonly head: infer H extends Readonly<Record<string, ClassedField>> } ? H : undefined
149
141
 
150
142
  /**
151
143
  * A recursive predicate REFERENCE — the shape `idb()` targets carry: the
152
- * name (type-level identity: a recursive rule's own `idb` accepts only its
153
- * own name the self-recursion cut), the runtime data (value identity),
154
- * the params its attached rules have used so far, and the head signature
155
- * its FIRST rule sealed (thread the value `.rule(...)` returns into an
156
- * `idb` and the program's `Params` type stays exact AND the idb join is
157
- * arity- and domain-checked against the head).
144
+ * name (type-level identity), the runtime data (value identity), the params
145
+ * its rules have used, and the head signature its FIRST rule sealed.
158
146
  */
159
147
  interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadShape = HeadShape> {
160
148
  readonly name: Name
@@ -162,44 +150,44 @@ interface RecRef<Name extends string, P extends ParamsRecord, Head extends HeadS
162
150
  readonly [inferred]?: { readonly params: P; readonly head: Head }
163
151
  }
164
152
 
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. */
166
- type IdbVarOk<Env extends EnvShape, T, F> =
167
- T extends Var<infer N extends string>
168
- ? N extends keyof Env
169
- ? F extends ClassedField
170
- ? JoinOk<Env[N], F>
171
- : true
153
+ /** One `idb` position's judgment: a variable class-equal to the head slot when the head is carried. */
154
+ type IdbBindingOk<Classes extends SchemaClasses, HeadSlot, V> = V extends AnyVar
155
+ ? HeadSlot extends ClassedField
156
+ ? JoinOk<HeadSlot, MintSlotOf<Classes, V>> extends true
157
+ ? true
172
158
  : false
173
- : false
159
+ : true
160
+ : false
174
161
 
175
162
  /**
176
- * The validated `idb` variable tuple: every var must already be bound by a
177
- * relation atom of the rule; and when the target carries its head
178
- * signature (the threaded rec handle), the tuple must match the head's
179
- * arity and every position must be class-equal to its head slot — the
180
- * same wall `JoinOk` holds for EDB atoms. An unthreaded handle carries no
181
- * head; its joins stay boundness-checked here and arity/class-judged at
182
- * prepare (the engine's law stands behind both tiers).
163
+ * The validated `idb` bindings record: when the target carries its head
164
+ * (a threaded rec handle), the record's key set must EXACTLY equal the
165
+ * head's (a missing or extra key maps every property to `never`) and each
166
+ * variable must be class-equal to its head slot — the same wall `JoinOk`
167
+ * holds for EDB atoms. An unthreaded handle carries no head; every entry
168
+ * must still be a variable, arity/class judged at construction and prepare.
183
169
  */
184
- type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> = Head extends readonly ClassedField[]
185
- ? V extends readonly unknown[]
186
- ? V["length"] extends Head["length"]
187
- ? { readonly [I in keyof V]: IdbVarOk<Env, V[I], Head[I & keyof Head]> extends true ? V[I] : never }
188
- : { readonly [I in keyof V]: never }
189
- : never
190
- : { readonly [I in keyof V]: IdbVarOk<Env, V[I], undefined> extends true ? V[I] : never }
170
+ type CheckIdbBindings<Classes extends SchemaClasses, Head, B> =
171
+ Head extends Readonly<Record<string, ClassedField>>
172
+ ? [keyof B] extends [keyof Head]
173
+ ? [keyof Head] extends [keyof B]
174
+ ? {
175
+ readonly [K in keyof B]: K extends keyof Head
176
+ ? IdbBindingOk<Classes, Head[K], B[K]> extends true
177
+ ? B[K]
178
+ : never
179
+ : never
180
+ }
181
+ : { readonly [K in keyof B]: never }
182
+ : { readonly [K in keyof B]: never }
183
+ : { readonly [K in keyof B]: B[K] extends AnyVar ? B[K] : never }
191
184
 
192
185
  /**
193
186
  * The term/predicate/aggregate constructor vocabulary every rule builder
194
- * carries — pure value builders, environment-free: the chain's `.where`,
195
- * `.match`, and `.select` seams judge their output against the rule
196
- * environment.
187
+ * carries — pure value builders. Variables are minted by the free {@link v},
188
+ * outside the rule, and reused by reference; `r` no longer mints them.
197
189
  */
198
190
  interface TermOps {
199
- /** Declares/names one variable: typed by the field it first binds; reuse joins. */
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
203
191
  /** Names one scalar parameter: typed by its use; the key of the execute params object. */
204
192
  readonly param: typeof makeParam
205
193
  /** Names one ∈-set parameter (the IR's `ParamSet`): bound to a readonly array at execution. */
@@ -215,7 +203,6 @@ interface TermOps {
215
203
  readonly gt: typeof gt
216
204
  readonly ge: typeof ge
217
205
  readonly pointIn: typeof pointIn
218
- readonly covers: typeof covers
219
206
  readonly allen: typeof allen
220
207
  readonly and: typeof and
221
208
  readonly or: typeof or
@@ -230,92 +217,68 @@ interface TermOps {
230
217
  readonly pack: typeof pack
231
218
  }
232
219
 
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). */
220
+ /** The rule builder a `query(S).rule(...)` callback receives (`Classes` — the join judge's authority). */
234
221
  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). */
222
+ /** The first EDB atom of the rule: fields bind variables, params, ∈-sets, or bare literals; absence is the wildcard. */
236
223
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
237
224
  relation: R,
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
- >
225
+ bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
226
+ ): QueryRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
245
227
  }
246
228
 
247
229
  /** The chain of a plain query rule: more atoms, residual predicates, then the head. */
248
230
  interface QueryRuleChain<
249
231
  Rels extends SchemaRelations,
250
- Env extends EnvShape,
251
232
  P extends ParamsRecord,
252
233
  Classes extends SchemaClasses = SchemaClasses
253
234
  > {
254
- /** One more positive EDB atom — var reuse joins, class-equal by the environment check. */
235
+ /** One more positive EDB atom — variable reuse joins, class-equal by the mint-slot judgment. */
255
236
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
256
237
  relation: R,
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
- >
238
+ bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
239
+ ): QueryRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
264
240
  /** One residual predicate: a comparison, an `and`/`or` tree, or a negated atom (`r.not`). */
265
241
  where<const C extends AnyCond>(
266
- cond: CheckCond<Env, Classes, C> & C
267
- ): QueryRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
268
- /** The head projection: var names, the measure, and aggregates; written order = answer column order. */
269
- select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
242
+ cond: CheckCond<Classes, C> & C
243
+ ): QueryRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
244
+ /** The head projection: a `find` RECORD whose keys name the answer columns. */
245
+ find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P>
270
246
  }
271
247
 
272
248
  /** The rule builder an OUTPUT rule of a `program()` receives: a query rule plus finished-stratum `idb` atoms. */
273
249
  interface OutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
274
250
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
275
251
  relation: R,
276
- bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
277
- ): OutputRuleChain<
278
- Rels,
279
- EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
280
- BindParamsShape<MatchFields<R>, B>,
281
- Classes
282
- >
252
+ bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
253
+ ): OutputRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
283
254
  }
284
255
 
285
256
  /** The chain of an output rule: atoms, predicates, `idb` joins over the program's recs, then the head. */
286
257
  interface OutputRuleChain<
287
258
  Rels extends SchemaRelations,
288
- Env extends EnvShape,
289
259
  P extends ParamsRecord,
290
260
  Classes extends SchemaClasses = SchemaClasses
291
261
  > {
292
262
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
293
263
  relation: R,
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
- >
264
+ bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
265
+ ): OutputRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
301
266
  where<const C extends AnyCond>(
302
- cond: CheckCond<Env, Classes, C> & C
303
- ): OutputRuleChain<Rels, Env, Flatten<P & CondParamsShape<Env, C>>, Classes>
267
+ cond: CheckCond<Classes, C> & C
268
+ ): OutputRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
304
269
  /**
305
270
  * One `idb` atom over a FINISHED stratum (any rec of this program): a
306
- * positional join against the rec's head. An idb atom is a join
307
- * position every variable must already be bound by a relation atom of
308
- * the rule (the theory's own domain relation; the rec's answers are
309
- * theory values, so the join is identity). Threading the rec value the
310
- * last `.rule(...)` returned carries its rules' params into `Params`
311
- * AND its head signature, so the join is arity- and class-checked
312
- * against the head at compile time.
271
+ * NAMED join against the rec's head the bindings record's keys are the
272
+ * head columns, each bound to a variable positively bound by a relation
273
+ * atom of the rule. Threading the rec value the last `.rule(...)` returned
274
+ * carries its params into `Params` AND its head signature, so the join is
275
+ * key-exact and class-checked against the head at compile time.
313
276
  */
314
- idb<Target extends RecRef<string, ParamsRecord>, const V extends readonly Var<string>[]>(
277
+ idb<Target extends RecRef<string, ParamsRecord>, const B extends Readonly<Record<string, AnyVar>>>(
315
278
  target: Target,
316
- ...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
317
- ): OutputRuleChain<Rels, Env, Flatten<P & ParamsOf<Target>>, Classes>
318
- select<const S extends readonly SelectEntry[]>(...entries: CheckSelect<Env, S> & S): RuleValue<RowOfSelect<Env, S>, P>
279
+ bindings: B & CheckIdbBindings<Classes, HeadOf<Target>, B>
280
+ ): OutputRuleChain<Rels, Flatten<P & ParamsOf<Target>>, Classes>
281
+ find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P>
319
282
  }
320
283
 
321
284
  /** The rule builder a RECURSIVE rule (`rec.rule(...)`) receives. */
@@ -323,53 +286,35 @@ interface RecRuleScope<Rels extends SchemaRelations, Self extends string, Classe
323
286
  extends TermOps {
324
287
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
325
288
  relation: R,
326
- bindings: B & CheckBindings<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
327
- ): RecRuleChain<
328
- Rels,
329
- Self,
330
- EnvOfMatch<Record<never, never>, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>,
331
- BindParamsShape<MatchFields<R>, B>,
332
- Classes
333
- >
289
+ bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
290
+ ): RecRuleChain<Rels, Self, BindParamsShape<MatchFields<R>, B>, Classes>
334
291
  }
335
292
 
336
293
  /**
337
- * The chain of a recursive rule. Its `idb` accepts ONLY the rec itself
338
- * the self-recursion cut as a type-level boundary (mutual recursion is
339
- * unwritable; a finished lower stratum is folded by the OUTPUT rules) —
340
- * and its `select` takes bound variable NAMES only: aggregates and the
341
- * measure are unrepresentable in a recursive head (the strata judge's
342
- * `AggregationThroughCycle`/`MeasureInRecursiveHead`, made unwritable).
294
+ * The chain of a recursive rule. Its `idb` accepts ONLY the rec itself (the
295
+ * self-recursion cut) and its `find` takes bound variables only — aggregates
296
+ * and the measure are unrepresentable in a recursive head.
343
297
  */
344
298
  interface RecRuleChain<
345
299
  Rels extends SchemaRelations,
346
300
  Self extends string,
347
- Env extends EnvShape,
348
301
  P extends ParamsRecord,
349
302
  Classes extends SchemaClasses = SchemaClasses
350
303
  > {
351
304
  match<R extends QueryRelation<Rels>, const B extends MatchShape<MatchFields<R>>>(
352
305
  relation: R,
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
- >
306
+ bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
307
+ ): RecRuleChain<Rels, Self, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
361
308
  where<const C extends AnyCond>(
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). */
365
- idb<Target extends RecRef<Self, ParamsRecord>, const V extends readonly Var<string>[]>(
309
+ cond: CheckCond<Classes, C> & C
310
+ ): RecRuleChain<Rels, Self, Flatten<P & CondParamsShape<C>>, Classes>
311
+ /** The self-recursive atom: `idb(self, { headKey: boundVar })` — only this rec's own reference is accepted. */
312
+ idb<Target extends RecRef<Self, ParamsRecord>, const B extends Readonly<Record<string, AnyVar>>>(
366
313
  target: Target,
367
- ...vars: CheckIdbVars<Env, V, HeadOf<Target>> & V
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. */
370
- select<const S extends readonly string[]>(
371
- ...names: CheckNameSelect<Env, S> & S
372
- ): RuleValue<RowOfNameSelect<Env, S>, P, HeadFieldsOf<Env, S>>
314
+ bindings: B & CheckIdbBindings<Classes, HeadOf<Target>, B>
315
+ ): RecRuleChain<Rels, Self, P, Classes>
316
+ /** The recursive head: a `find` record of bound variables only; the value carries the head's classed slots for `idb` pairing. */
317
+ find<const F extends FindShape>(entries: F & CheckRecFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
373
318
  }
374
319
 
375
320
  /** A query's runtime description — everything lowering, the wire marshal, and answer decode read. */
@@ -379,16 +324,15 @@ interface QueryData {
379
324
  /** The output rules in written order (multiple rules = set union). */
380
325
  readonly rules: readonly RuleData[]
381
326
  /** The head columns (every rule derives the same head; written order = answer column order). */
382
- readonly select: readonly SelectColumn[]
327
+ readonly finds: readonly FindColumn[]
383
328
  /** The registered params in first-use order across the program walk (= dense `ParamId`s). */
384
329
  readonly params: readonly ParamEntry[]
385
330
  }
386
331
 
387
332
  /**
388
333
  * An inert query value. `Row` is the inferred answer-row object type;
389
- * `Params` the inferred execute-params object type — exactly the params
390
- * the rules use. Prepare with `db.prepare(q)`; nothing here touches an
391
- * engine.
334
+ * `Params` the inferred execute-params object type — exactly the params the
335
+ * rules use. Prepare with `db.prepare(q)`.
392
336
  */
393
337
  interface Query<
394
338
  Rels extends SchemaRelations,
@@ -405,11 +349,7 @@ interface Query<
405
349
  readonly [inferred]?: { readonly row: Row; readonly params: Params }
406
350
  }
407
351
 
408
- /**
409
- * Any query value as lowering and the runtime consume it: the theory it
410
- * was built against and its runtime description — every `Query` (typed or
411
- * program-built) carries exactly this.
412
- */
352
+ /** Any query value as lowering and the runtime consume it. */
413
353
  interface AnyQuery {
414
354
  readonly schema: AnySchema
415
355
  readonly data: QueryData
@@ -430,8 +370,6 @@ interface QueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses
430
370
 
431
371
  /** The frozen constructor vocabulary every rule builder spreads. */
432
372
  const termOps: TermOps = Object.freeze({
433
- var: makeVar,
434
- vars: makeVars,
435
373
  param: makeParam,
436
374
  inSet: makeSetParam,
437
375
  maskParam: makeMaskParam,
@@ -443,7 +381,6 @@ const termOps: TermOps = Object.freeze({
443
381
  gt,
444
382
  ge,
445
383
  pointIn,
446
- covers,
447
384
  allen,
448
385
  and,
449
386
  or,
@@ -458,64 +395,57 @@ const termOps: TermOps = Object.freeze({
458
395
  pack
459
396
  })
460
397
 
461
- /** One rule under construction: immutable — every chain step is a fresh state. */
398
+ /** One rule under construction: immutable — every chain step is a fresh state. Boundness rides the `bound` set of var references. */
462
399
  interface RuleBuildState {
463
400
  readonly items: readonly RuleItem[]
464
- readonly varFields: Readonly<Record<string, ClassedField>>
401
+ readonly bound: ReadonlySet<AnyVar>
465
402
  readonly paramUses: readonly ParamUse[]
466
403
  }
467
404
 
468
405
  /** The empty rule state. */
469
406
  const EMPTY_RULE: RuleBuildState = Object.freeze({
470
407
  items: Object.freeze([]),
471
- varFields: Object.freeze({}),
408
+ bound: new Set<AnyVar>(),
472
409
  paramUses: Object.freeze([])
473
410
  })
474
411
 
475
- /** One resolved bindings record: the atom entries, the vars it binds (as classed slots), and the params it uses. */
412
+ /** One resolved bindings record: the atom entries, the variable references it binds, and the params it uses. */
476
413
  interface ResolvedBindings {
477
414
  readonly atom: AtomData
478
- readonly vars: ReadonlyArray<{ readonly name: string; readonly slot: ClassedField }>
415
+ readonly vars: readonly AnyVar[]
479
416
  readonly uses: readonly ParamUse[]
480
417
  }
481
418
 
482
419
  /**
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.
420
+ * The MINT slot of a variable, the runtime twin of {@link MintSlotOf}: (i)
421
+ * verifies the mint owner is the schema's own member value — a variable
422
+ * minted from a foreign relation is refused, naming its label and (ii)
423
+ * returns the descriptor it was minted at plus the law-computed class read
424
+ * off the schema's frozen class map. Because {@link fieldJoins} is an
425
+ * equality, judging every binding position against this one slot makes all
426
+ * cross-binding joins mutually class-equal by transitivity.
490
427
  */
491
- function matchFieldsOf(owner: MatchOwner): readonly RelationField[] {
492
- if ("axioms" in owner) {
493
- return [Object.freeze({ name: "id", field: owner.id }), ...owner.data.columns]
428
+ function mintSlotOf(context: ChainContext, ref: AnyVar): ClassedField {
429
+ if (context.theory.relations[ref.owner.name] !== ref.owner) {
430
+ throw errors.new(
431
+ `the variable ${ref.label} was minted from a relation schema ${context.theory.name} does not declare — mint variables with v() from the schema's own relations`
432
+ )
494
433
  }
495
- return owner.data.fields
434
+ return { field: ref.field, class: context.classes[ref.owner.name]?.[ref.column] }
496
435
  }
497
436
 
498
437
  /**
499
438
  * 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.
439
+ * CLOSED-reference field, holding 2 DISTINCT handle names. The returned
440
+ * name is CONTENT-ADDRESSED (vocabulary + the member SET).
512
441
  */
513
442
  function membershipSet(
514
443
  context: string,
515
444
  field: AnyField,
516
445
  value: readonly unknown[]
517
446
  ): { readonly name: string; readonly members: readonly string[] } {
518
- if (!("closed" in field)) {
447
+ const roster = rosterOf(field)
448
+ if (roster === undefined) {
519
449
  throw errors.new(
520
450
  `${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`
521
451
  )
@@ -531,7 +461,7 @@ function membershipSet(
531
461
  const seen = new Set<string>()
532
462
  const members = value.map(function memberName(member) {
533
463
  if (typeof member !== "string") {
534
- throw literalShapeError(context, `a ${field.closed.name} handle name (string)`, member)
464
+ throw literalShapeError(context, `a ${roster.name} handle name (string)`, member)
535
465
  }
536
466
  if (seen.has(member)) {
537
467
  throw errors.new(
@@ -542,29 +472,28 @@ function membershipSet(
542
472
  return member
543
473
  })
544
474
  const key = [...members].sort()
545
- return { name: `∈ ${field.closed.name} ${JSON.stringify(key)}`, members: Object.freeze(members) }
475
+ return { name: `∈ ${roster.name} ${JSON.stringify(key)}`, members: Object.freeze(members) }
546
476
  }
547
477
 
548
478
  /**
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
479
+ * Resolves a bindings record against an atom owner's matchable fields, in
551
480
  * 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.
481
+ * everything else is a bare literal. Every VARIABLE binding judges
482
+ * `fieldJoins(mintSlot, positionSlot)` and throws on a class-unequal reuse
483
+ * (the runtime twin of `CheckBindings`); the bound refs are collected for
484
+ * the rule's boundness set.
556
485
  */
557
486
  function resolveBindings(
558
- context: string,
487
+ context: ChainContext,
488
+ label: string,
559
489
  relation: MatchOwner,
560
- bindings: Readonly<Record<string, unknown>>,
561
- classes: SchemaClasses
490
+ bindings: Readonly<Record<string, unknown>>
562
491
  ): ResolvedBindings {
563
492
  const entries: BindingEntry[] = []
564
- const vars: Array<{ readonly name: string; readonly slot: ClassedField }> = []
493
+ const vars: AnyVar[] = []
565
494
  const uses: ParamUse[] = []
566
- const relationClasses = classes[relation.name]
567
- const ordered = matchFieldsOf(relation)
495
+ const relationClasses = context.classes[relation.name]
496
+ const ordered = sealedFieldsOf(relation)
568
497
  for (const [fieldName, value] of Object.entries(bindings)) {
569
498
  if (value === undefined) {
570
499
  continue
@@ -573,17 +502,23 @@ function resolveBindings(
573
502
  return candidate.name === fieldName
574
503
  })
575
504
  if (declared === undefined) {
576
- throw errors.new(`${context} has no field ${fieldName}`)
505
+ throw errors.new(`${label} has no field ${fieldName}`)
577
506
  }
578
507
  const fieldClass = relationClasses?.[fieldName]
579
508
  let bound: BindingEntry["term"]
580
509
  if (isTerm(value)) {
581
510
  switch (value[term]) {
582
511
  case "var": {
583
- bound = Object.freeze({ kind: "var" as const, name: value.name })
584
- vars.push(
585
- Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) })
586
- )
512
+ const ref = value
513
+ const mint = mintSlotOf(context, ref)
514
+ const positionSlot: ClassedField = { field: declared.field, class: fieldClass }
515
+ if (!fieldJoins(mint, positionSlot)) {
516
+ throw errors.new(
517
+ `${label}: the variable ${ref.label} joins domain-unequal fields — minted at ${renderFieldKind(mint)}, reused at ${renderFieldKind(positionSlot)} (a var joins only class-equal slots; bare pairs only with bare)`
518
+ )
519
+ }
520
+ bound = Object.freeze({ kind: "var" as const, ref })
521
+ vars.push(ref)
587
522
  break
588
523
  }
589
524
  case "param": {
@@ -614,15 +549,15 @@ function resolveBindings(
614
549
  }
615
550
  case "maskParam":
616
551
  throw errors.new(
617
- `${context}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`
552
+ `${label}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`
618
553
  )
619
554
  case "duration":
620
555
  throw errors.new(
621
- `${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`
556
+ `${label}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and find entries`
622
557
  )
623
558
  }
624
559
  } else if (Array.isArray(value)) {
625
- const set = membershipSet(`${context}.${fieldName}`, declared.field, value)
560
+ const set = membershipSet(`${label}.${fieldName}`, declared.field, value)
626
561
  bound = Object.freeze({ kind: "literalSet" as const, name: set.name, members: set.members })
627
562
  uses.push(
628
563
  Object.freeze({
@@ -638,57 +573,40 @@ function resolveBindings(
638
573
  }
639
574
  entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }))
640
575
  }
641
- return {
642
- atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
643
- vars,
644
- uses
645
- }
576
+ return { atom: Object.freeze({ relation, bindings: Object.freeze(entries) }), vars, uses }
646
577
  }
647
578
 
648
- /**
649
- * Extends a rule state with one positive atom. Vars bind on first
650
- * occurrence; every LATER occurrence (a later atom's field or a same-record
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.
654
- */
579
+ /** Extends a rule state with one positive atom; the bound variable references accumulate into the boundness set. */
655
580
  function advanceMatch(
581
+ context: ChainContext,
656
582
  state: RuleBuildState,
657
583
  relation: MatchOwner,
658
- bindings: Readonly<Record<string, unknown>>,
659
- classes: SchemaClasses
584
+ bindings: Readonly<Record<string, unknown>>
660
585
  ): RuleBuildState {
661
- const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes)
662
- const varFields: Record<string, ClassedField> = { ...state.varFields }
663
- for (const bound of resolved.vars) {
664
- const existing = varFields[bound.name]
665
- if (existing === undefined) {
666
- varFields[bound.name] = bound.slot
667
- } else if (!fieldJoins(existing, bound.slot)) {
668
- throw errors.new(
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)`
670
- )
671
- }
586
+ const resolved = resolveBindings(context, `relation ${relation.name}`, relation, bindings)
587
+ const bound = new Set(state.bound)
588
+ for (const ref of resolved.vars) {
589
+ bound.add(ref)
672
590
  }
673
- return {
591
+ return Object.freeze({
674
592
  items: Object.freeze([...state.items, Object.freeze({ kind: "atom" as const, atom: resolved.atom })]),
675
- varFields: Object.freeze(varFields),
593
+ bound,
676
594
  paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
677
- }
595
+ })
678
596
  }
679
597
 
680
- /** Resolves one comparison side to its runtime term. */
598
+ /** Resolves one comparison side to its runtime term (variables and the measure ride by reference). */
681
599
  function cmpTermDataOf(op: string, value: unknown): CmpTermData {
682
600
  if (isTerm(value)) {
683
601
  switch (value[term]) {
684
602
  case "var":
685
- return Object.freeze({ kind: "var" as const, name: value.name })
603
+ return Object.freeze({ kind: "var" as const, ref: value })
686
604
  case "param":
687
605
  return Object.freeze({ kind: "param" as const, name: value.name })
688
606
  case "setParam":
689
607
  return Object.freeze({ kind: "setParam" as const, name: value.name })
690
608
  case "duration":
691
- return Object.freeze({ kind: "measure" as const, name: value.name })
609
+ return Object.freeze({ kind: "measure" as const, ref: value.over })
692
610
  case "maskParam":
693
611
  throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`)
694
612
  }
@@ -698,23 +616,16 @@ function cmpTermDataOf(op: string, value: unknown): CmpTermData {
698
616
 
699
617
  /**
700
618
  * One comparison side's contribution to the param census: a param/set side
701
- * anchors to its SIBLING — a bound variable's field descriptor or the
702
- * measure; an unanchorable use (literal or param sibling) records with no
703
- * anchor and must be anchored by some other use of the same name.
619
+ * anchors to its SIBLING — a variable's field descriptor or the measure; an
620
+ * unanchorable use records with no anchor.
704
621
  */
705
- function sideUses(
706
- op: CmpKind,
707
- side: CmpTermData,
708
- sibling: CmpTermData,
709
- varFields: Readonly<Record<string, ClassedField>>,
710
- uses: ParamUse[]
711
- ): void {
622
+ function sideUses(op: CmpKind, side: CmpTermData, sibling: CmpTermData, uses: ParamUse[]): void {
712
623
  if (side.kind !== "param" && side.kind !== "setParam") {
713
624
  return
714
625
  }
715
626
  let anchor: AnyField | "measure" | undefined
716
627
  if (sibling.kind === "var") {
717
- anchor = varFields[sibling.name]?.field
628
+ anchor = sibling.ref.field
718
629
  } else if (sibling.kind === "measure") {
719
630
  anchor = "measure"
720
631
  } else {
@@ -732,12 +643,12 @@ function sideUses(
732
643
  }
733
644
 
734
645
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
735
- function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedField>>, uses: ParamUse[]): CondData {
646
+ function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
736
647
  if (cond.cond === "cmp") {
737
648
  const lhs = cmpTermDataOf(cond.op, cond.lhs)
738
649
  const rhs = cmpTermDataOf(cond.op, cond.rhs)
739
- sideUses(cond.op, lhs, rhs, varFields, uses)
740
- sideUses(cond.op, rhs, lhs, varFields, uses)
650
+ sideUses(cond.op, lhs, rhs, uses)
651
+ sideUses(cond.op, rhs, lhs, uses)
741
652
  let mask: MaskData | undefined
742
653
  if (cond.op === "allen") {
743
654
  const maskValue = cond.mask
@@ -758,15 +669,13 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedFie
758
669
  throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
759
670
  }
760
671
  }
761
- const data: CmpData = Object.freeze({ kind: "cmp" as const, op: cond.op, mask, lhs, rhs })
762
- return data
672
+ return Object.freeze({ kind: "cmp" as const, op: cond.op, mask, lhs, rhs })
763
673
  }
764
674
  if (cond.cond === "tree") {
765
675
  const children = cond.children.map(function lowerChild(child) {
766
- return condDataOf(child, varFields, uses)
676
+ return condDataOf(child, uses)
767
677
  })
768
- const data: TreeData = Object.freeze({ kind: "tree" as const, op: cond.op, children: Object.freeze(children) })
769
- return data
678
+ return Object.freeze({ kind: "tree" as const, op: cond.op, children: Object.freeze(children) })
770
679
  }
771
680
  throw errors.new(
772
681
  "a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()"
@@ -774,7 +683,7 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedFie
774
683
  }
775
684
 
776
685
  /** Extends a rule state with one `.where` item (a condition or a negated atom). */
777
- function advanceWhere(state: RuleBuildState, cond: AnyCond, classes: SchemaClasses): RuleBuildState {
686
+ function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCond): RuleBuildState {
778
687
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
779
688
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
780
689
  }
@@ -785,136 +694,139 @@ function advanceWhere(state: RuleBuildState, cond: AnyCond, classes: SchemaClass
785
694
  return value !== undefined
786
695
  })
787
696
  )
788
- const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes)
789
- return {
697
+ const resolved = resolveBindings(context, `negated relation ${relation.name}`, relation, bindings)
698
+ return Object.freeze({
790
699
  items: Object.freeze([...state.items, Object.freeze({ kind: "negated" as const, atom: resolved.atom })]),
791
- varFields: state.varFields,
700
+ bound: state.bound,
792
701
  paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
793
- }
702
+ })
794
703
  }
795
704
  const uses: ParamUse[] = []
796
- const data = condDataOf(cond, state.varFields, uses)
797
- return {
705
+ const data = condDataOf(cond, uses)
706
+ return Object.freeze({
798
707
  items: Object.freeze([...state.items, Object.freeze({ kind: "cond" as const, cond: data })]),
799
- varFields: state.varFields,
708
+ bound: state.bound,
800
709
  paramUses: Object.freeze([...state.paramUses, ...uses])
801
- }
710
+ })
802
711
  }
803
712
 
804
- /** Extends a rule state with one `idb` atom (vars must be bound validated at completion). */
805
- function advanceIdb(state: RuleBuildState, rec: RecData, vars: readonly Var<string>[]): RuleBuildState {
806
- const names = vars.map(function nameOf(variable) {
807
- if (!isTerm(variable) || variable[term] !== "var") {
808
- throw errors.new(`idb ${rec.name}: positions take variables — bind literals and params through where()/match()`)
713
+ /** Extends a rule state with one `idb` atom (a named record over head keys; vars validated at completion). */
714
+ function advanceIdb(state: RuleBuildState, rec: RecData, bindings: Readonly<Record<string, unknown>>): RuleBuildState {
715
+ const resolved: Array<{ readonly key: string; readonly ref: AnyVar }> = []
716
+ for (const [key, value] of Object.entries(bindings)) {
717
+ if (value === undefined) {
718
+ continue
809
719
  }
810
- return variable.name
811
- })
812
- return {
813
- items: Object.freeze([...state.items, Object.freeze({ kind: "idb" as const, rec, vars: Object.freeze(names) })]),
814
- varFields: state.varFields,
815
- paramUses: state.paramUses
720
+ if (!isTerm(value) || value[term] !== "var") {
721
+ throw errors.new(
722
+ `idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`
723
+ )
724
+ }
725
+ resolved.push(Object.freeze({ key, ref: value }))
816
726
  }
727
+ return Object.freeze({
728
+ items: Object.freeze([
729
+ ...state.items,
730
+ Object.freeze({ kind: "idb" as const, rec, bindings: Object.freeze(resolved) })
731
+ ]),
732
+ bound: state.bound,
733
+ paramUses: state.paramUses
734
+ })
817
735
  }
818
736
 
819
- /** Narrows a select entry to an aggregate value. */
737
+ /** Narrows a find entry to an aggregate value. */
820
738
  function isAggregateEntry(
821
739
  value: unknown
822
740
  ): value is { readonly agg: string; readonly over: unknown; readonly key: unknown } {
823
741
  return typeof value === "object" && value !== null && "agg" in value
824
742
  }
825
743
 
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
- */
832
- function selectColumnOf(entry: unknown): SelectColumn {
833
- if (typeof entry === "string") {
834
- return Object.freeze({
835
- name: entry,
836
- entry: Object.freeze({ kind: "var" as const, over: entry }),
837
- closed: undefined
838
- })
839
- }
840
- if (isTerm(entry)) {
841
- if (entry[term] === "duration") {
842
- return Object.freeze({
843
- name: entry.name,
844
- entry: Object.freeze({ kind: "measure" as const, over: entry.name }),
845
- closed: undefined
846
- })
847
- }
848
- throw errors.new(
849
- `query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`
850
- )
744
+ /** Narrows a value to a variable term, else a pointed refusal. */
745
+ function asVarTerm(context: string, value: unknown): AnyVar {
746
+ if (isTerm(value) && value[term] === "var") {
747
+ return value
851
748
  }
852
- if (isAggregateEntry(entry)) {
853
- return aggregateColumnOf(entry)
854
- }
855
- throw errors.new("query select: not a select entry — select takes variable names, duration(v), or aggregates")
749
+ throw errors.new(`${context}: expected a variable`)
856
750
  }
857
751
 
858
- /** Classifies one aggregate select entry. */
859
- function aggregateColumnOf(entry: {
860
- readonly agg: string
861
- readonly over: unknown
862
- readonly key: unknown
863
- }): SelectColumn {
864
- function column(name: string, agg: AggData): SelectColumn {
865
- return Object.freeze({
866
- name,
867
- entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }),
868
- closed: undefined
869
- })
870
- }
752
+ /** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
753
+ function aggDataOf(
754
+ name: string,
755
+ entry: { readonly agg: string; readonly over: unknown; readonly key: unknown }
756
+ ): AggData {
871
757
  const over = entry.over
872
758
  switch (entry.agg) {
873
759
  case "count":
874
- return column("count", { op: "count" })
875
- case "countDistinct": {
876
- if (typeof over !== "string") {
877
- throw errors.new("countDistinct takes a variable name")
878
- }
879
- return column(over, { op: "countDistinct", over })
880
- }
760
+ return Object.freeze({ op: "count" as const })
761
+ case "countDistinct":
762
+ return Object.freeze({ op: "countDistinct" as const, over: asVarTerm(`find ${name} (countDistinct)`, over) })
881
763
  case "sum":
882
764
  case "min":
883
765
  case "max": {
884
- if (typeof over === "string") {
885
- return column(over, { op: "fold", fold: entry.agg, over })
766
+ if (isTerm(over) && over[term] === "var") {
767
+ return Object.freeze({ op: "fold" as const, fold: entry.agg, over })
886
768
  }
887
769
  if (isTerm(over) && over[term] === "duration") {
888
- return column(over.name, { op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.name }) })
770
+ return Object.freeze({ op: "fold" as const, fold: entry.agg, over: Object.freeze({ duration: over.over }) })
889
771
  }
890
- throw errors.new(`${entry.agg} takes a variable name or duration(v)`)
772
+ throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`)
891
773
  }
892
774
  case "argMax":
893
- case "argMin": {
894
- if (typeof over !== "string" || typeof entry.key !== "string") {
895
- throw errors.new(`${entry.agg} takes a carried variable name and an orderable key variable name`)
896
- }
897
- return column(over, { op: "arg", direction: entry.agg, over, key: entry.key })
775
+ case "argMin":
776
+ return Object.freeze({
777
+ op: "arg" as const,
778
+ direction: entry.agg,
779
+ over: asVarTerm(`find ${name} (${entry.agg})`, over),
780
+ key: asVarTerm(`find ${name} (${entry.agg} key)`, entry.key)
781
+ })
782
+ case "pack":
783
+ return Object.freeze({ op: "pack" as const, over: asVarTerm(`find ${name} (pack)`, over) })
784
+ default:
785
+ throw errors.new(`find ${name}: unknown aggregate ${entry.agg}`)
786
+ }
787
+ }
788
+
789
+ /**
790
+ * Classifies one find entry into its named answer column (the KEY names the
791
+ * column, `count` included). The `slot`/`closed` slices are resolved LATER,
792
+ * at rule completion, where boundness and the mint slots are in hand.
793
+ */
794
+ function findColumnOf(name: string, entry: unknown): FindColumn {
795
+ if (isTerm(entry)) {
796
+ if (entry[term] === "var") {
797
+ return Object.freeze({
798
+ name,
799
+ entry: Object.freeze({ kind: "var" as const, over: entry }),
800
+ closed: undefined,
801
+ slot: undefined
802
+ })
898
803
  }
899
- case "pack": {
900
- if (typeof over !== "string") {
901
- throw errors.new("pack takes a variable name")
902
- }
903
- return column(over, { op: "pack", over })
804
+ if (entry[term] === "duration") {
805
+ return Object.freeze({
806
+ name,
807
+ entry: Object.freeze({ kind: "measure" as const, over: entry.over }),
808
+ closed: undefined,
809
+ slot: undefined
810
+ })
904
811
  }
905
- default:
906
- throw errors.new(`unknown aggregate ${entry.agg}`)
812
+ throw errors.new(
813
+ `find ${name}: a ${entry[term]} is not projectable — find takes variables, r.duration(v), or aggregates`
814
+ )
815
+ }
816
+ if (isAggregateEntry(entry)) {
817
+ return Object.freeze({
818
+ name,
819
+ entry: Object.freeze({ kind: "aggregate" as const, agg: aggDataOf(name, entry) }),
820
+ closed: undefined,
821
+ slot: undefined
822
+ })
907
823
  }
824
+ throw errors.new(`find ${name}: not a find entry — find takes variables, r.duration(v), or aggregates`)
908
825
  }
909
826
 
910
827
  /**
911
828
  * 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).
829
+ * § orderability): a closed reference is equality-and-membership only.
918
830
  */
919
831
  function closedOrderError(context: string, position: string, vocabulary: string): Error {
920
832
  return errors.new(
@@ -922,81 +834,62 @@ function closedOrderError(context: string, position: string, vocabulary: string)
922
834
  )
923
835
  }
924
836
 
925
- /** The comparison ops the orderable ban covers (order roster + point membership — every order-comparison position). */
837
+ /** The comparison ops under the orderable ban (order roster + point membership). */
926
838
  function isOrderOp(op: CmpKind | "binding"): op is "lt" | "le" | "gt" | "ge" | "pointIn" {
927
839
  return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn"
928
840
  }
929
841
 
930
- /** Requires a var name to be bound by a relation atom of the rule. */
931
- function assertBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): ClassedField {
932
- const slot = varFields[name]
933
- if (slot === undefined) {
934
- throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`)
842
+ /** Requires a variable to be bound by a relation atom of the rule (the boundness wall — invisible to the type tier). */
843
+ function assertBound(where: string, bound: ReadonlySet<AnyVar>, ref: AnyVar): void {
844
+ if (!bound.has(ref)) {
845
+ throw errors.new(`${where}: the variable ${ref.label} is not bound by a relation atom of the rule`)
935
846
  }
936
- return slot
937
847
  }
938
848
 
939
- /** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
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") {
849
+ /** Requires a variable to be interval-typed (the measure's and pack's domain), off its own descriptor. */
850
+ function assertInterval(where: string, ref: AnyVar): void {
851
+ if (ref.field.kind !== "interval") {
943
852
  throw errors.new(
944
- `${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`
853
+ `${where}: ${ref.label} is not interval-typed — the measure is defined over interval-typed variables only`
945
854
  )
946
855
  }
947
856
  }
948
857
 
858
+ /** Requires a variable's own field to be non-closed (the orderable ban's runtime twin). */
859
+ function assertNotClosed(where: string, position: string, ref: AnyVar): void {
860
+ const roster = rosterOf(ref.field)
861
+ if (roster !== undefined) {
862
+ throw closedOrderError(where, `${position} ${ref.label}`, roster.name)
863
+ }
864
+ }
865
+
949
866
  /**
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.
867
+ * The classed mint slot one answer column's VALUES flow from: a projected
868
+ * variable's mint slot, or an Arg-carried payload's. Counts, folds, `pack`
869
+ * and the measure derive numbers/intervals, so they resolve no slot.
957
870
  */
958
- function validateCond(context: string, varFields: Readonly<Record<string, ClassedField>>, cond: CondData): void {
959
- if (cond.kind === "cmp") {
960
- for (const side of [cond.lhs, cond.rhs]) {
961
- if (side.kind === "var") {
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
- }
966
- }
967
- if (side.kind === "measure") {
968
- assertIntervalBound(context, varFields, side.name)
969
- }
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
- }
980
- return
871
+ function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedField | undefined {
872
+ const entry = column.entry
873
+ if (entry.kind === "var") {
874
+ return mintSlotOf(context, entry.over)
981
875
  }
982
- for (const child of cond.children) {
983
- validateCond(context, varFields, child)
876
+ if (entry.kind === "aggregate" && entry.agg.op === "arg") {
877
+ return mintSlotOf(context, entry.agg.over)
984
878
  }
879
+ return undefined
985
880
  }
986
881
 
987
- /** Validates one select column's variable references. */
988
- function validateColumn(
989
- context: string,
990
- varFields: Readonly<Record<string, ClassedField>>,
991
- column: SelectColumn
992
- ): void {
882
+ /** Validates one find column's variable references (boundness + the orderable/interval walls, off the var's own field). */
883
+ function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, column: FindColumn): void {
884
+ const where = `${contextLabel(context)} find ${column.name}`
993
885
  const entry = column.entry
994
886
  if (entry.kind === "var") {
995
- assertBound(`${context} select ${column.name}`, varFields, entry.over)
887
+ assertBound(where, bound, entry.over)
996
888
  return
997
889
  }
998
890
  if (entry.kind === "measure") {
999
- assertIntervalBound(`${context} select ${column.name}`, varFields, entry.over)
891
+ assertBound(where, bound, entry.over)
892
+ assertInterval(where, entry.over)
1000
893
  return
1001
894
  }
1002
895
  const agg = entry.agg
@@ -1004,151 +897,163 @@ function validateColumn(
1004
897
  case "count":
1005
898
  return
1006
899
  case "countDistinct":
1007
- assertBound(`${context} select ${column.name}`, varFields, agg.over)
900
+ assertBound(where, bound, agg.over)
1008
901
  return
1009
902
  case "fold": {
1010
- if (typeof agg.over === "string") {
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
- }
903
+ if ("duration" in agg.over) {
904
+ assertBound(where, bound, agg.over.duration)
905
+ assertInterval(where, agg.over.duration)
1019
906
  return
1020
907
  }
1021
- assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration)
908
+ assertBound(where, bound, agg.over)
909
+ assertNotClosed(where, `the ${agg.fold} input`, agg.over)
1022
910
  return
1023
911
  }
1024
912
  case "arg": {
1025
- assertBound(`${context} select ${column.name}`, varFields, agg.over)
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
- }
913
+ assertBound(where, bound, agg.over)
914
+ assertBound(where, bound, agg.key)
915
+ assertNotClosed(where, `the ${agg.direction} key`, agg.key)
1034
916
  return
1035
917
  }
1036
918
  case "pack":
1037
- assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over)
919
+ assertBound(where, bound, agg.over)
920
+ assertInterval(where, agg.over)
1038
921
  return
1039
922
  }
1040
923
  }
1041
924
 
1042
925
  /**
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.
926
+ * Validates one condition's variable references against the rule's bound
927
+ * set and, for `eq`/`ne` over two variables, holds the class wall through
928
+ * the mint slots (the unification IS a join; bare pairs only with bare).
1051
929
  */
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
930
+ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: CondData): void {
931
+ const label = contextLabel(context)
932
+ if (cond.kind === "cmp") {
933
+ for (const side of [cond.lhs, cond.rhs]) {
934
+ if (side.kind === "var") {
935
+ assertBound(label, bound, side.ref)
936
+ const roster = rosterOf(side.ref.field)
937
+ if (isOrderOp(cond.op) && roster !== undefined) {
938
+ throw closedOrderError(label, `the ${cond.op} side ${side.ref.label}`, roster.name)
939
+ }
940
+ }
941
+ if (side.kind === "measure") {
942
+ assertBound(label, bound, side.ref)
943
+ assertInterval(label, side.ref)
944
+ }
945
+ }
946
+ if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
947
+ assertBound(label, bound, cond.lhs.ref)
948
+ assertBound(label, bound, cond.rhs.ref)
949
+ const lhs = mintSlotOf(context, cond.lhs.ref)
950
+ const rhs = mintSlotOf(context, cond.rhs.ref)
951
+ if (!fieldJoins(lhs, rhs)) {
952
+ throw errors.new(
953
+ `${label}: ${cond.op}(${cond.lhs.ref.label}, ${cond.rhs.ref.label}) unifies domain-unequal fields — ${cond.lhs.ref.label} bound at ${renderFieldKind(lhs)}, ${cond.rhs.ref.label} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
954
+ )
955
+ }
956
+ }
957
+ return
1066
958
  }
1067
- const field = varFields[over]?.field
1068
- if (field !== undefined && "closed" in field) {
1069
- return field.closed
959
+ for (const child of cond.children) {
960
+ validateCond(context, bound, child)
1070
961
  }
1071
- return undefined
1072
962
  }
1073
963
 
1074
964
  /**
1075
- * Completes one rule: classifies the select record (written order = answer
1076
- * column order, names must be declaration-order-safe keys), and validates
1077
- * boundness — every condition/select/idb variable bound by a relation atom,
1078
- * and every NEGATED atom's variable positively bound (the safety rule: a
1079
- * negated atom binds nothing, only rejects).
965
+ * Validates one `idb` item: every head column of the rec is bound exactly
966
+ * once (a missing or extra key is a pointed error), every bound variable is
967
+ * positively bound by a relation atom of the rule, and each variable joins
968
+ * its head column's classed slot. When the rec's own rule 0 is in flight
969
+ * (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
970
+ * head.
1080
971
  */
1081
- function completeRule(context: string, state: RuleBuildState, columns: readonly SelectColumn[]): RuleData {
1082
- if (columns.length === 0) {
1083
- throw errors.new(`${context}: a select needs at least one entry`)
972
+ function validateIdb(
973
+ context: ChainContext,
974
+ bound: ReadonlySet<AnyVar>,
975
+ item: { readonly rec: RecData; readonly bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }> },
976
+ columns: readonly FindColumn[]
977
+ ): void {
978
+ const label = contextLabel(context)
979
+ const head = item.rec.rules[0]
980
+ const headColumns = head !== undefined ? head.finds : columns
981
+ const headNames = headColumns.map(function nameOf(column) {
982
+ return column.name
983
+ })
984
+ const keys = item.bindings.map(function keyOf(binding) {
985
+ return binding.key
986
+ })
987
+ for (const key of keys) {
988
+ if (!headNames.includes(key)) {
989
+ throw errors.new(
990
+ `${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`
991
+ )
992
+ }
1084
993
  }
1085
- const seen = new Set<string>()
1086
- for (const column of columns) {
1087
- assertDeclarationOrderKey(`${context} select column`, column.name)
1088
- if (seen.has(column.name)) {
1089
- throw errors.new(`${context}: select names the answer column ${column.name} twice`)
994
+ for (const name of headNames) {
995
+ if (!keys.includes(name)) {
996
+ throw errors.new(
997
+ `${label}: idb ${item.rec.name} omits the head column ${name} — an idb join binds every head column of ${item.rec.name}`
998
+ )
999
+ }
1000
+ }
1001
+ for (const binding of item.bindings) {
1002
+ if (!bound.has(binding.ref)) {
1003
+ throw errors.new(
1004
+ `${label}: idb ${item.rec.name} names the variable ${binding.ref.label}, but no relation atom of the rule binds it — an idb atom is a join position; bind the variable through the theory's own relation first`
1005
+ )
1006
+ }
1007
+ const headColumn = headColumns.find(function byName(column) {
1008
+ return column.name === binding.key
1009
+ })
1010
+ if (headColumn === undefined || headColumn.slot === undefined) {
1011
+ continue
1012
+ }
1013
+ const mint = mintSlotOf(context, binding.ref)
1014
+ if (!fieldJoins(headColumn.slot, mint)) {
1015
+ throw errors.new(
1016
+ `${label}: idb ${item.rec.name} joins the variable ${binding.ref.label} (${renderFieldKind(mint)}) at head column ${binding.key} (${renderFieldKind(headColumn.slot)}) — a var joins only class-equal slots; bare pairs only with bare`
1017
+ )
1090
1018
  }
1091
- seen.add(column.name)
1092
- validateColumn(context, state.varFields, column)
1093
1019
  }
1020
+ }
1021
+
1022
+ /**
1023
+ * Completes one rule: enriches the find columns (declaration-order-safe
1024
+ * keys, boundness validated, each column's classed slot and closed slice
1025
+ * resolved), then walks the body walls — negated-atom boundness safety, idb
1026
+ * head pairing, and condition validation.
1027
+ */
1028
+ function completeRule(context: ChainContext, state: RuleBuildState, rawColumns: readonly FindColumn[]): RuleData {
1029
+ const label = contextLabel(context)
1030
+ if (rawColumns.length === 0) {
1031
+ throw errors.new(`${label}: a find needs at least one entry`)
1032
+ }
1033
+ const columns = rawColumns.map(function enrichColumn(column): FindColumn {
1034
+ assertDeclarationOrderKey(`${label} find column`, column.name)
1035
+ validateColumn(context, state.bound, column)
1036
+ const slot = findColumnSlotOf(context, column)
1037
+ return Object.freeze({ name: column.name, entry: column.entry, slot, closed: rosterOf(slot?.field) })
1038
+ })
1094
1039
  for (const item of state.items) {
1095
1040
  if (item.kind === "negated") {
1096
1041
  for (const binding of item.atom.bindings) {
1097
- if (binding.term.kind === "var") {
1098
- const bound = state.varFields[binding.term.name]
1099
- if (bound === undefined) {
1100
- throw errors.new(
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)`
1102
- )
1103
- }
1104
- const negatedSlot: ClassedField = { field: binding.data, class: binding.class }
1105
- if (!fieldJoins(bound, negatedSlot)) {
1106
- throw errors.new(
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`
1108
- )
1109
- }
1042
+ if (binding.term.kind === "var" && !state.bound.has(binding.term.ref)) {
1043
+ throw errors.new(
1044
+ `${label}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.ref.label} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
1045
+ )
1110
1046
  }
1111
1047
  }
1112
1048
  }
1113
1049
  if (item.kind === "idb") {
1114
- const head = item.rec.rules[0]
1115
- item.vars.forEach(function checkIdbVar(name, position) {
1116
- const bound = state.varFields[name]
1117
- if (bound === undefined) {
1118
- throw errors.new(
1119
- `${context}: idb ${item.rec.name} names the variable ${name}, but no relation atom of the rule binds it — an idb atom is a join position; bind the variable through the theory's own relation first`
1120
- )
1121
- }
1122
- const column = head?.select[position]
1123
- if (column === undefined || column.entry.kind !== "var") {
1124
- return
1125
- }
1126
- const headSlot = head?.varFields[column.entry.over]
1127
- if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
1128
- throw errors.new(
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`
1130
- )
1131
- }
1132
- })
1050
+ validateIdb(context, state.bound, item, columns)
1133
1051
  }
1134
1052
  if (item.kind === "cond") {
1135
- validateCond(context, state.varFields, item.cond)
1053
+ validateCond(context, state.bound, item.cond)
1136
1054
  }
1137
1055
  }
1138
- return Object.freeze({
1139
- items: state.items,
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
- ),
1149
- varFields: state.varFields,
1150
- paramUses: state.paramUses
1151
- })
1056
+ return Object.freeze({ items: state.items, finds: Object.freeze(columns), paramUses: state.paramUses })
1152
1057
  }
1153
1058
 
1154
1059
  /** Builds one typed rule value over completed rule data. */
@@ -1157,19 +1062,14 @@ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<R
1157
1062
  }
1158
1063
 
1159
1064
  /**
1160
- * The one runtime chain every context shares — non-generic on purpose: the
1161
- * typed chain interfaces (`QueryRuleChain`/`OutputRuleChain`/`RecRuleChain`)
1162
- * apply at the scope factories' boundaries, and the runtime beneath them is
1163
- * one plain value walk. Context gates the two context-bound verbs: `idb`
1164
- * (a program construct — self-only inside a rec, any rec of the program in
1165
- * the output, refused in a plain query) and the recursive `select`
1166
- * (bound variable names only — the creation quarantine).
1065
+ * The one runtime chain every context shares — non-generic on purpose. The
1066
+ * typed chain interfaces apply at the scope factories' boundaries.
1167
1067
  */
1168
1068
  interface RawChain {
1169
1069
  match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1170
1070
  where(cond: AnyCond): RawChain
1171
- idb(target: RecRef<string, ParamsRecord>, ...vars: readonly Var<string>[]): RawChain
1172
- select(...entries: readonly SelectEntry[]): RuleValue<never, never>
1071
+ idb(target: RecRef<string, ParamsRecord>, bindings: Readonly<Record<string, unknown>>): RawChain
1072
+ find(entries: Readonly<Record<string, unknown>>): RuleValue<never, never>
1173
1073
  }
1174
1074
 
1175
1075
  /** The runtime rule-builder shape beneath every typed scope. */
@@ -1177,8 +1077,8 @@ interface RawScope extends TermOps {
1177
1077
  match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1178
1078
  }
1179
1079
 
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 } & (
1080
+ /** Which rule family a chain builds — plus the schema's runtime class map and theory value (the join judge's authority). */
1081
+ type ChainContext = { readonly classes: SchemaClasses; readonly theory: AnySchema } & (
1182
1082
  | { readonly kind: "query" }
1183
1083
  | { readonly kind: "rec"; readonly self: RecData }
1184
1084
  | { readonly kind: "output"; readonly program: ProgramState }
@@ -1201,7 +1101,7 @@ function idbAdvance(
1201
1101
  context: ChainContext,
1202
1102
  state: RuleBuildState,
1203
1103
  target: RecRef<string, ParamsRecord>,
1204
- vars: readonly Var<string>[]
1104
+ bindings: Readonly<Record<string, unknown>>
1205
1105
  ): RuleBuildState {
1206
1106
  if (context.kind === "query") {
1207
1107
  throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()")
@@ -1212,42 +1112,47 @@ function idbAdvance(
1212
1112
  `rec ${context.self.name}: a recursive rule's idb target must be the rec itself — the self-recursion-only cut (mutual recursion is unwritable; fold a finished stratum in the output rules)`
1213
1113
  )
1214
1114
  }
1215
- return advanceIdb(state, context.self, vars)
1115
+ return advanceIdb(state, context.self, bindings)
1216
1116
  }
1217
1117
  if (!context.program.recs.includes(target.data)) {
1218
1118
  throw errors.new(
1219
1119
  `idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`
1220
1120
  )
1221
1121
  }
1222
- return advanceIdb(state, target.data, vars)
1122
+ return advanceIdb(state, target.data, bindings)
1223
1123
  }
1224
1124
 
1225
- /** Classifies one select tuple per the context (a recursive head projects bound NAMES only). */
1226
- function selectColumns(context: ChainContext, entries: readonly SelectEntry[]): SelectColumn[] {
1227
- return entries.map(function columnOf(entry): SelectColumn {
1228
- if (context.kind === "rec" && typeof entry !== "string") {
1125
+ /** Classifies one find record per the context (a recursive head projects bound variables only). */
1126
+ function findColumns(context: ChainContext, entries: Readonly<Record<string, unknown>>): FindColumn[] {
1127
+ const columns: FindColumn[] = []
1128
+ for (const [name, entry] of Object.entries(entries)) {
1129
+ if (entry === undefined) {
1130
+ continue
1131
+ }
1132
+ if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
1229
1133
  throw errors.new(
1230
- `rec ${context.self.name}: a recursive head projects bound variable NAMES only — aggregates and the measure read finished sets (the strata judge's quarantine, unwritable here)`
1134
+ `rec ${context.self.name}: a recursive head projects bound variables only — aggregates and the measure read finished sets (the strata judge's quarantine, unwritable here)`
1231
1135
  )
1232
1136
  }
1233
- return selectColumnOf(entry)
1234
- })
1137
+ columns.push(findColumnOf(name, entry))
1138
+ }
1139
+ return columns
1235
1140
  }
1236
1141
 
1237
1142
  /** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
1238
1143
  function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
1239
1144
  const chain: RawChain = {
1240
1145
  match(relation, bindings) {
1241
- return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes))
1146
+ return makeRawChain(context, advanceMatch(context, state, relation, bindings))
1242
1147
  },
1243
1148
  where(cond) {
1244
- return makeRawChain(context, advanceWhere(state, cond, context.classes))
1149
+ return makeRawChain(context, advanceWhere(context, state, cond))
1245
1150
  },
1246
- idb(target, ...vars) {
1247
- return makeRawChain(context, idbAdvance(context, state, target, vars))
1151
+ idb(target, bindings) {
1152
+ return makeRawChain(context, idbAdvance(context, state, target, bindings))
1248
1153
  },
1249
- select(...entries) {
1250
- return makeRuleValue<never, never>(completeRule(contextLabel(context), state, selectColumns(context, entries)))
1154
+ find(entries) {
1155
+ return makeRuleValue<never, never>(completeRule(context, state, findColumns(context, entries)))
1251
1156
  }
1252
1157
  }
1253
1158
  Object.freeze(chain)
@@ -1259,7 +1164,7 @@ function makeRawScope(context: ChainContext): RawScope {
1259
1164
  const scope: RawScope = {
1260
1165
  ...termOps,
1261
1166
  match(relation, bindings) {
1262
- return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes))
1167
+ return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings))
1263
1168
  }
1264
1169
  }
1265
1170
  Object.freeze(scope)
@@ -1267,13 +1172,13 @@ function makeRawScope(context: ChainContext): RawScope {
1267
1172
  }
1268
1173
 
1269
1174
  /**
1270
- * The one trusted admission seam of the rule builders (the `relation()`
1271
- * `refsComplete` pattern): the raw builder is one runtime shape for every
1272
- * context, and this guard verifies the checkable fact — the builder verbs
1273
- * exist — before the value is admitted at its TYPED face. The type-level
1274
- * judgments (domain-equal joins, boundness, the recursion cut) live in the
1275
- * interfaces themselves; the runtime twin of every one of them is a
1276
- * construction-time validation in this module.
1175
+ * The rule builders' trusted admission seam THE home of the
1176
+ * trusted-admission-seam pattern the other mint guards cite: the raw builder
1177
+ * is one runtime shape for every context, and this guard verifies the
1178
+ * checkable fact — the builder verbs exist — before the value is admitted at
1179
+ * its TYPED face. The type-level judgments (class-equal joins, the recursion
1180
+ * cut) live in the interfaces themselves; boundness is a construction-time
1181
+ * validation in this module (object identity is invisible to the type tier).
1277
1182
  */
1278
1183
  function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
1279
1184
  return typeof scope.match === "function"
@@ -1281,9 +1186,9 @@ function isTypedScope<S>(scope: RawScope): scope is RawScope & S {
1281
1186
 
1282
1187
  /** Builds one query-rule builder (the typed face of the raw builder). */
1283
1188
  function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1284
- classes: SchemaClasses
1189
+ theory: AnySchema
1285
1190
  ): QueryRuleScope<Rels, Classes> {
1286
- const raw = makeRawScope({ kind: "query", classes })
1191
+ const raw = makeRawScope({ kind: "query", classes: theory.classes, theory })
1287
1192
  if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
1288
1193
  throw errors.new("query rule builder construction incomplete")
1289
1194
  }
@@ -1294,17 +1199,18 @@ function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends Schema
1294
1199
  function makeOutputRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1295
1200
  program: ProgramState
1296
1201
  ): OutputRuleScope<Rels, Classes> {
1297
- const raw = makeRawScope({ kind: "output", program, classes: program.classes })
1202
+ const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory })
1298
1203
  if (!isTypedScope<OutputRuleScope<Rels, Classes>>(raw)) {
1299
1204
  throw errors.new("program output rule builder construction incomplete")
1300
1205
  }
1301
1206
  return raw
1302
1207
  }
1303
1208
 
1304
- /** One program's build-time registry: its recs in declaration order (sealed when the output is declared) and the theory's class map. */
1209
+ /** One program's build-time registry: its recs in declaration order, the theory value, and its class map. */
1305
1210
  interface ProgramState {
1306
1211
  readonly recs: RecData[]
1307
1212
  readonly classes: SchemaClasses
1213
+ readonly theory: AnySchema
1308
1214
  sealed: boolean
1309
1215
  }
1310
1216
 
@@ -1314,7 +1220,7 @@ function renderClosedSlice(closed: ClosedRoster | undefined): string {
1314
1220
  }
1315
1221
 
1316
1222
  /** Renders one head column's signature for the rule-alignment check. */
1317
- function headSignature(column: SelectColumn): string {
1223
+ function headSignature(column: FindColumn): string {
1318
1224
  const entry = column.entry
1319
1225
  if (entry.kind === "var" || entry.kind === "measure") {
1320
1226
  return `${column.name}:var`
@@ -1331,13 +1237,7 @@ function headSignature(column: SelectColumn): string {
1331
1237
 
1332
1238
  /** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
1333
1239
  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
1240
+ return anchor === "measure" ? undefined : rosterOf(anchor)
1341
1241
  }
1342
1242
 
1343
1243
  /** Renders one param anchor's closedness for the registry's coherence diagnostics. */
@@ -1347,20 +1247,9 @@ function renderParamAnchor(roster: ClosedRoster | undefined): string {
1347
1247
 
1348
1248
  /**
1349
1249
  * Folds every rule's param uses (recs in declaration order first, output
1350
- * rules last — exactly the lowering walk) into the query's registry:
1351
- * first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
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).
1250
+ * rules last — exactly the lowering walk) into the query's registry: first
1251
+ * use mints the dense `ParamId`, the first FIELD-ANCHORED use types the
1252
+ * wire, and one name keeps one shape AND one closedness.
1364
1253
  */
1365
1254
  function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
1366
1255
  const order: string[] = []
@@ -1430,13 +1319,9 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1430
1319
  if (entry === undefined) {
1431
1320
  throw errors.new(`query param ${name} lost its registry entry`)
1432
1321
  }
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)
1322
+ const anchorRoster = anchorRosterOf(entry.anchor)
1323
+ if (entry.orderOp !== undefined && anchorRoster !== undefined) {
1324
+ throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name)
1440
1325
  }
1441
1326
  return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members })
1442
1327
  })
@@ -1453,47 +1338,58 @@ interface RawQuery {
1453
1338
  /**
1454
1339
  * Assembles the runtime query value over completed rules: every rule must
1455
1340
  * derive the SAME head (name and aggregate shape, position for position —
1456
- * the decode labels and the engine's alignment rule agree by
1457
- * construction), and the param registry folds in program-walk order.
1341
+ * the decode labels and the engine's alignment rule agree), and the param
1342
+ * registry folds in program-walk order.
1458
1343
  */
1459
1344
  function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readonly RuleData[]): RawQuery {
1460
1345
  const first = rules[0]
1461
1346
  if (first === undefined) {
1462
1347
  throw errors.new("a query needs at least one rule")
1463
1348
  }
1464
- const signature = first.select.map(headSignature).join(", ")
1349
+ const signature = first.finds.map(headSignature).join(", ")
1465
1350
  rules.forEach(function verifyHead(rule, index) {
1466
- const candidate = rule.select.map(headSignature).join(", ")
1351
+ const candidate = rule.finds.map(headSignature).join(", ")
1467
1352
  if (candidate !== signature) {
1468
1353
  throw errors.new(
1469
- `every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`
1354
+ `every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
1470
1355
  )
1471
1356
  }
1472
1357
  // 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]
1358
+ // through one roster, so a union whose rules bind a column at different
1359
+ // vocabularies (or one closed, one bare) is refused pointed.
1360
+ rule.finds.forEach(function verifyClosedSlice(column, position) {
1361
+ const lead = first.finds[position]
1479
1362
  if (lead !== undefined && column.closed !== lead.closed) {
1480
1363
  throw errors.new(
1481
1364
  `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
1365
  )
1483
1366
  }
1367
+ // The law-class wall on the union head: one answer column is one
1368
+ // value space, so the classed mint slot each rule binds the column
1369
+ // at must join across rules — the SAME fieldJoins judgment every
1370
+ // join/eq/negated-atom position enforces (the SDK holds it because
1371
+ // the wire IR carries no domains).
1372
+ if (lead === undefined) {
1373
+ return
1374
+ }
1375
+ if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
1376
+ throw errors.new(
1377
+ `every rule of a query derives the same head — the answer column ${lead.name} unions domain-unequal fields: bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in rule ${index} (a union column joins only class-equal slots; bare pairs only with bare)`
1378
+ )
1379
+ }
1484
1380
  })
1485
1381
  })
1486
1382
  const data: QueryData = Object.freeze({
1487
1383
  recs: Object.freeze([...recs]),
1488
1384
  rules: Object.freeze([...rules]),
1489
- select: first.select,
1385
+ finds: first.finds,
1490
1386
  params: paramRegistryOf(recs, rules)
1491
1387
  })
1492
1388
  const value: RawQuery = {
1493
1389
  schema: theory,
1494
1390
  data,
1495
1391
  rule(build) {
1496
- const built = build(makeRawScope({ kind: "query", classes: theory.classes }))
1392
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }))
1497
1393
  return makeRawQuery(theory, recs, [...rules, built.rule])
1498
1394
  }
1499
1395
  }
@@ -1502,7 +1398,7 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
1502
1398
  }
1503
1399
 
1504
1400
  /**
1505
- * The query values' trusted admission seam (the `refsComplete` pattern):
1401
+ * The query values' trusted admission seam (the {@link isTypedScope} pattern):
1506
1402
  * the checkable fact — the value was assembled over the identical theory —
1507
1403
  * is verified before the raw value is admitted at its typed face.
1508
1404
  */
@@ -1527,11 +1423,10 @@ function makeQuery<Rels extends SchemaRelations, Row, P extends ParamsRecord, Cl
1527
1423
  }
1528
1424
 
1529
1425
  /**
1530
- * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
1531
- * adds one conjunctive rule; multiple rules are the set union (answers are
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.
1426
+ * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
1427
+ * one conjunctive rule; multiple rules are the set union. The schema's
1428
+ * law-computed class map and theory value ride into every rule builder — the
1429
+ * join walls compare against the mint slots off it.
1535
1430
  */
1536
1431
  function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1537
1432
  theory: Schema<Rels, Classes>
@@ -1540,7 +1435,7 @@ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1540
1435
  rule<RV extends AnyRuleValue>(
1541
1436
  build: (r: QueryRuleScope<Rels, Classes>) => RV
1542
1437
  ): Query<Rels, RowOf<RV>, ParamsOf<RV>, Classes> {
1543
- const built = build(makeQueryRuleScope<Rels, Classes>(theory.classes))
1438
+ const built = build(makeQueryRuleScope<Rels, Classes>(theory))
1544
1439
  return makeQuery<Rels, RowOf<RV>, ParamsOf<RV>, Classes>(theory, [], [built.rule])
1545
1440
  }
1546
1441
  }
@@ -1548,32 +1443,10 @@ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1548
1443
  return start
1549
1444
  }
1550
1445
 
1551
- /** The typed shape refusal of the literal tagger — a genuine failure, never data. */
1552
- function literalShapeError(context: string, expected: string, value: unknown): Error {
1553
- return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
1554
- }
1555
-
1556
- /** Narrows an interval-shaped literal (a plain `{ start, end }` bigint pair). */
1557
- function isIntervalShaped(value: unknown): value is { readonly start: bigint; readonly end: bigint } {
1558
- return (
1559
- typeof value === "object" &&
1560
- value !== null &&
1561
- "start" in value &&
1562
- "end" in value &&
1563
- typeof value.start === "bigint" &&
1564
- typeof value.end === "bigint"
1565
- )
1566
- }
1567
-
1568
1446
  /**
1569
1447
  * 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).
1448
+ * roster and translated to its declaration-order row id, tagged u64. THE
1449
+ * single roster-verification point of the query surface.
1577
1450
  */
1578
1451
  function taggedHandleId(
1579
1452
  context: string,
@@ -1604,7 +1477,7 @@ function taggedAtElementDomain(context: string, element: "u64" | "i64", value: u
1604
1477
  }
1605
1478
  return { kind: "i64", value }
1606
1479
  }
1607
- if (isIntervalShaped(value)) {
1480
+ if (isIntervalValue(value)) {
1608
1481
  if (element === "u64") {
1609
1482
  return { kind: "intervalU64", start: value.start, end: value.end }
1610
1483
  }
@@ -1615,15 +1488,12 @@ function taggedAtElementDomain(context: string, element: "u64" | "i64", value: u
1615
1488
 
1616
1489
  /**
1617
1490
  * Tags one host literal at a FIELD position (atom bindings): the field's
1618
- * structural kind directs the tag, never a guess. At an interval field a
1619
- * bigint literal tags as the ELEMENT type — the IR's membership typing
1620
- * rule (point membership), an interval-shaped literal as the interval
1621
- * (value equality). A closed-reference literal is its bare handle id,
1622
- * tagged u64 after a roster verification.
1491
+ * structural kind directs the tag, never a guess.
1623
1492
  */
1624
1493
  function taggedLiteral(context: string, field: AnyField, value: unknown): TaggedValue {
1625
- if ("closed" in field) {
1626
- return taggedHandleId(context, field.closed, value)
1494
+ const roster = rosterOf(field)
1495
+ if (roster !== undefined) {
1496
+ return taggedHandleId(context, roster, value)
1627
1497
  }
1628
1498
  switch (field.kind) {
1629
1499
  case "bool": {
@@ -1648,6 +1518,9 @@ function taggedLiteral(context: string, field: AnyField, value: unknown): Tagged
1648
1518
  if (typeof value !== "string") {
1649
1519
  throw literalShapeError(context, "string", value)
1650
1520
  }
1521
+ if (!value.isWellFormed()) {
1522
+ throw literalShapeError(context, "well-formed string", value)
1523
+ }
1651
1524
  return { kind: "string", value }
1652
1525
  }
1653
1526
  case "bytes": {
@@ -1662,17 +1535,13 @@ function taggedLiteral(context: string, field: AnyField, value: unknown): Tagged
1662
1535
  }
1663
1536
 
1664
1537
  /**
1665
- * Tags one host literal at a COMPARISON or PARAM position, where the
1666
- * SIBLING anchors the type: a measure sibling is u64, an interval-field
1667
- * sibling contributes its element domain (so both a point literal in
1668
- * `covers` and a `span` literal in `allen` tag correctly), a scalar
1669
- * sibling its own type. At `pointIn` the operand order is interval-left,
1670
- * point-right (`ir::CmpOp::PointIn`), so an interval-shaped literal
1671
- * beside a scalar element-typed sibling is the LEGAL lhs of
1672
- * `covers(span(...), t)` and tags as the interval of the sibling's
1673
- * element domain; under every other operator an interval shape against a
1674
- * scalar sibling stays refused (the engine's IllegalComparison — the
1675
- * bug-hunt fix, preserved op-aware).
1538
+ * Tags one host literal at a COMPARISON or PARAM position, where the SIBLING
1539
+ * anchors the type: a measure sibling is u64, an interval-field sibling
1540
+ * contributes its element domain, a scalar sibling its own type. At
1541
+ * `pointIn` the operand order is interval-left, point-right, so an
1542
+ * interval-shaped literal beside a scalar element-typed sibling is the LEGAL
1543
+ * interval operand of `pointIn(t, span(...))`; under every other operator an
1544
+ * interval shape against a scalar sibling stays refused.
1676
1545
  */
1677
1546
  function taggedCmpLiteral(
1678
1547
  context: string,
@@ -1686,14 +1555,14 @@ function taggedCmpLiteral(
1686
1555
  }
1687
1556
  return { kind: "u64", value }
1688
1557
  }
1689
- if (!("closed" in sibling) && sibling.kind === "interval") {
1558
+ if (rosterOf(sibling) === undefined && sibling.kind === "interval") {
1690
1559
  return taggedAtElementDomain(context, sibling.element, value)
1691
1560
  }
1692
1561
  if (
1693
1562
  op === "pointIn" &&
1694
- !("closed" in sibling) &&
1563
+ rosterOf(sibling) === undefined &&
1695
1564
  (sibling.kind === "u64" || sibling.kind === "i64") &&
1696
- isIntervalShaped(value)
1565
+ isIntervalValue(value)
1697
1566
  ) {
1698
1567
  return taggedAtElementDomain(context, sibling.kind, value)
1699
1568
  }
@@ -1709,22 +1578,22 @@ interface LowerContext {
1709
1578
  readonly params: ReadonlyMap<string, ParamEntry>
1710
1579
  }
1711
1580
 
1712
- /** One rule's dense variable numbering: first occurrence in written order. */
1581
+ /** One rule's dense variable numbering: first occurrence in written order, keyed on the object REFERENCE. */
1713
1582
  interface VarIds {
1714
- of(name: string): number
1583
+ of(ref: AnyVar): number
1715
1584
  }
1716
1585
 
1717
1586
  /** Creates one rule-scoped variable numberer. */
1718
- function makeVarIds(): VarIds {
1719
- const assigned = new Map<string, number>()
1587
+ function freshVarIds(): VarIds {
1588
+ const assigned = new Map<AnyVar, number>()
1720
1589
  return {
1721
- of(name) {
1722
- const existing = assigned.get(name)
1590
+ of(ref) {
1591
+ const existing = assigned.get(ref)
1723
1592
  if (existing !== undefined) {
1724
1593
  return existing
1725
1594
  }
1726
1595
  const id = assigned.size
1727
- assigned.set(name, id)
1596
+ assigned.set(ref, id)
1728
1597
  return id
1729
1598
  }
1730
1599
  }
@@ -1741,10 +1610,7 @@ function paramIdOf(ctx: LowerContext, name: string): number {
1741
1610
 
1742
1611
  /**
1743
1612
  * 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).
1613
+ * same edb source, with field ordinals over the SEALED shape.
1748
1614
  */
1749
1615
  function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1750
1616
  const member = ctx.theory.relations[atom.relation.name]
@@ -1757,7 +1623,7 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1757
1623
  if (relationId === undefined) {
1758
1624
  throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`)
1759
1625
  }
1760
- const ordered = matchFieldsOf(atom.relation)
1626
+ const ordered = sealedFieldsOf(atom.relation)
1761
1627
  const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
1762
1628
  const ordinal = ordered.findIndex(function byName(candidate) {
1763
1629
  return candidate.name === binding.field
@@ -1770,17 +1636,12 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1770
1636
  return { source: { kind: "edb", relation: relationId }, bindings }
1771
1637
  }
1772
1638
 
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
- */
1639
+ /** Lowers one binding term. A membership ARRAY lowers to the existing param-set term over its content-addressed entry. */
1779
1640
  function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEntry, ids: VarIds): TermIr {
1780
1641
  const bound = binding.term
1781
1642
  switch (bound.kind) {
1782
1643
  case "var":
1783
- return { kind: "var", var: ids.of(bound.name) }
1644
+ return { kind: "var", var: ids.of(bound.ref) }
1784
1645
  case "param":
1785
1646
  return { kind: "param", param: paramIdOf(ctx, bound.name) }
1786
1647
  case "setParam":
@@ -1792,42 +1653,51 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
1792
1653
  }
1793
1654
  }
1794
1655
 
1795
- /** Lowers one idb atom: positional head bindings, `FieldId(i)` = head position i. */
1796
- function lowerIdbAtom(ctx: LowerContext, rec: RecData, vars: readonly string[], ids: VarIds): AtomIr {
1656
+ /**
1657
+ * Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
1658
+ * head position i. Every head column of the rec must be bound (a missing key
1659
+ * is refused pointed); the var-id assignment order is head order, so the
1660
+ * first-use numbering matches the name-keyed edition exactly.
1661
+ */
1662
+ function lowerIdbAtom(
1663
+ ctx: LowerContext,
1664
+ rec: RecData,
1665
+ bindings: ReadonlyArray<{ readonly key: string; readonly ref: AnyVar }>,
1666
+ ids: VarIds
1667
+ ): AtomIr {
1797
1668
  const pred = ctx.recIds.get(rec)
1798
1669
  if (pred === undefined) {
1799
1670
  throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`)
1800
1671
  }
1801
- const arity = rec.rules[0]?.select.length
1802
- if (arity !== undefined && vars.length !== arity) {
1803
- throw errors.new(`query lowering: idb ${rec.name} takes ${arity} positions, got ${vars.length}`)
1672
+ const head = rec.rules[0]
1673
+ if (head === undefined) {
1674
+ throw errors.new(`query lowering: rec ${rec.name} has no rules`)
1804
1675
  }
1805
- const bindings: Array<readonly [number, TermIr]> = vars.map(function lowerPosition(name, position) {
1806
- return [position, { kind: "var", var: ids.of(name) } as const] as const
1676
+ const irBindings: Array<readonly [number, TermIr]> = head.finds.map(function lowerPosition(column, position) {
1677
+ const binding = bindings.find(function byKey(candidate) {
1678
+ return candidate.key === column.name
1679
+ })
1680
+ if (binding === undefined) {
1681
+ throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`)
1682
+ }
1683
+ return [position, { kind: "var", var: ids.of(binding.ref) } as const] as const
1807
1684
  })
1808
- return { source: { kind: "idb", pred }, bindings }
1685
+ return { source: { kind: "idb", pred }, bindings: irBindings }
1809
1686
  }
1810
1687
 
1811
1688
  /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
1812
- function lowerCmpTerm(
1813
- ctx: LowerContext,
1814
- rule: RuleData,
1815
- side: CmpTermData,
1816
- sibling: CmpTermData,
1817
- ids: VarIds,
1818
- op: CmpKind
1819
- ): TermIr {
1689
+ function lowerCmpTerm(ctx: LowerContext, side: CmpTermData, sibling: CmpTermData, ids: VarIds, op: CmpKind): TermIr {
1820
1690
  switch (side.kind) {
1821
1691
  case "var":
1822
- return { kind: "var", var: ids.of(side.name) }
1692
+ return { kind: "var", var: ids.of(side.ref) }
1823
1693
  case "param":
1824
1694
  return { kind: "param", param: paramIdOf(ctx, side.name) }
1825
1695
  case "setParam":
1826
1696
  return { kind: "paramSet", param: paramIdOf(ctx, side.name) }
1827
1697
  case "measure":
1828
- return { kind: "measure", var: ids.of(side.name) }
1698
+ return { kind: "measure", var: ids.of(side.ref) }
1829
1699
  case "literal": {
1830
- const anchor = cmpAnchorOf(ctx, rule, sibling)
1700
+ const anchor = cmpAnchorOf(ctx, sibling)
1831
1701
  if (anchor === undefined) {
1832
1702
  throw errors.new(
1833
1703
  "query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it"
@@ -1838,10 +1708,10 @@ function lowerCmpTerm(
1838
1708
  }
1839
1709
  }
1840
1710
 
1841
- /** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
1842
- function cmpAnchorOf(ctx: LowerContext, rule: RuleData, sibling: CmpTermData): AnyField | "measure" | undefined {
1711
+ /** Resolves the anchor a comparison literal tags by: the sibling variable's field, the measure, or an anchored param. */
1712
+ function cmpAnchorOf(ctx: LowerContext, sibling: CmpTermData): AnyField | "measure" | undefined {
1843
1713
  if (sibling.kind === "var") {
1844
- return rule.varFields[sibling.name]?.field
1714
+ return sibling.ref.field
1845
1715
  }
1846
1716
  if (sibling.kind === "measure") {
1847
1717
  return "measure"
@@ -1853,7 +1723,7 @@ function cmpAnchorOf(ctx: LowerContext, rule: RuleData, sibling: CmpTermData): A
1853
1723
  }
1854
1724
 
1855
1725
  /** Lowers one comparison. */
1856
- function lowerComparison(ctx: LowerContext, rule: RuleData, cmp: CmpData, ids: VarIds): ComparisonIr {
1726
+ function lowerComparison(ctx: LowerContext, cmp: CmpData, ids: VarIds): ComparisonIr {
1857
1727
  if (cmp.op === "allen") {
1858
1728
  const maskData = cmp.mask
1859
1729
  if (maskData === undefined) {
@@ -1865,32 +1735,32 @@ function lowerComparison(ctx: LowerContext, rule: RuleData, cmp: CmpData, ids: V
1865
1735
  : { kind: "param" as const, param: paramIdOf(ctx, maskData.name) }
1866
1736
  return {
1867
1737
  op: { kind: "allen", mask },
1868
- lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, "allen"),
1869
- rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, "allen")
1738
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
1739
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
1870
1740
  }
1871
1741
  }
1872
1742
  return {
1873
1743
  op: { kind: cmp.op },
1874
- lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, cmp.op),
1875
- rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, cmp.op)
1744
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
1745
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op)
1876
1746
  }
1877
1747
  }
1878
1748
 
1879
1749
  /** Lowers one condition node (comparison leaf or and/or tree). */
1880
- function lowerCondition(ctx: LowerContext, rule: RuleData, cond: CondData, ids: VarIds): ConditionTreeIr {
1750
+ function lowerCondition(ctx: LowerContext, cond: CondData, ids: VarIds): ConditionTreeIr {
1881
1751
  if (cond.kind === "cmp") {
1882
- return { kind: "leaf", cmp: lowerComparison(ctx, rule, cond, ids) }
1752
+ return { kind: "leaf", cmp: lowerComparison(ctx, cond, ids) }
1883
1753
  }
1884
1754
  return {
1885
1755
  kind: cond.op,
1886
1756
  children: cond.children.map(function lowerChild(child) {
1887
- return lowerCondition(ctx, rule, child, ids)
1757
+ return lowerCondition(ctx, child, ids)
1888
1758
  })
1889
1759
  }
1890
1760
  }
1891
1761
 
1892
- /** Lowers one select entry to its per-rule find term. */
1893
- function lowerFind(entry: SelectEntryData, ids: VarIds): FindTermIr {
1762
+ /** Lowers one find entry to its per-rule find term. */
1763
+ function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
1894
1764
  if (entry.kind === "var") {
1895
1765
  return { kind: "var", var: ids.of(entry.over) }
1896
1766
  }
@@ -1904,10 +1774,10 @@ function lowerFind(entry: SelectEntryData, ids: VarIds): FindTermIr {
1904
1774
  case "countDistinct":
1905
1775
  return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) }
1906
1776
  case "fold": {
1907
- if (typeof agg.over === "string") {
1908
- return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
1777
+ if ("duration" in agg.over) {
1778
+ return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
1909
1779
  }
1910
- return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
1780
+ return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
1911
1781
  }
1912
1782
  case "arg":
1913
1783
  return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) }
@@ -1932,8 +1802,8 @@ function headOpOf(agg: AggData): HeadOpIr {
1932
1802
  }
1933
1803
  }
1934
1804
 
1935
- /** One select entry's var-free head shape. */
1936
- function headTermOf(column: SelectColumn): HeadTermIr {
1805
+ /** One find entry's var-free head shape. */
1806
+ function headTermOf(column: FindColumn): HeadTermIr {
1937
1807
  const entry = column.entry
1938
1808
  if (entry.kind === "var" || entry.kind === "measure") {
1939
1809
  return { kind: "var" }
@@ -1943,7 +1813,7 @@ function headTermOf(column: SelectColumn): HeadTermIr {
1943
1813
 
1944
1814
  /** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
1945
1815
  function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1946
- const ids = makeVarIds()
1816
+ const ids = freshVarIds()
1947
1817
  const atoms: AtomIr[] = []
1948
1818
  const negated: AtomIr[] = []
1949
1819
  const conditions: ConditionTreeIr[] = []
@@ -1958,17 +1828,17 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1958
1828
  break
1959
1829
  }
1960
1830
  case "idb": {
1961
- atoms.push(lowerIdbAtom(ctx, item.rec, item.vars, ids))
1831
+ atoms.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids))
1962
1832
  break
1963
1833
  }
1964
1834
  case "cond": {
1965
- conditions.push(lowerCondition(ctx, rule, item.cond, ids))
1835
+ conditions.push(lowerCondition(ctx, item.cond, ids))
1966
1836
  break
1967
1837
  }
1968
1838
  }
1969
1839
  }
1970
1840
  return {
1971
- finds: rule.select.map(function findOf(column) {
1841
+ finds: rule.finds.map(function findOf(column) {
1972
1842
  return lowerFind(column.entry, ids)
1973
1843
  }),
1974
1844
  atoms,
@@ -1980,11 +1850,7 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
1980
1850
  /**
1981
1851
  * Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
1982
1852
  * recs in declaration order (`PredId` = index), the output predicate
1983
- * (rules + head) appended last. Relations lower by declaration ordinal,
1984
- * the law the engine's own manifest pins; `db.prepare` re-verifies the
1985
- * alignment against the live manifest before sending. Every registered
1986
- * param must carry a field anchor by now — an unanchorable param (its
1987
- * every use beside a literal) is refused here, naming it.
1853
+ * appended last. Every registered param must carry a field anchor by now.
1988
1854
  */
1989
1855
  function lowerQuery(q: AnyQuery): ProgramIr {
1990
1856
  const theory = q.schema
@@ -2014,14 +1880,14 @@ function lowerQuery(q: AnyQuery): ProgramIr {
2014
1880
  throw errors.new(`query lowering: rec ${rec.name} has no rules`)
2015
1881
  }
2016
1882
  return {
2017
- head: head.select.map(headTermOf),
1883
+ head: head.finds.map(headTermOf),
2018
1884
  rules: rec.rules.map(function lowerRecRule(rule) {
2019
1885
  return lowerRule(ctx, rule)
2020
1886
  })
2021
1887
  }
2022
1888
  })
2023
1889
  predicates.push({
2024
- head: q.data.select.map(headTermOf),
1890
+ head: q.data.finds.map(headTermOf),
2025
1891
  rules: q.data.rules.map(function lowerOutputRule(rule) {
2026
1892
  return lowerRule(ctx, rule)
2027
1893
  })
@@ -2032,7 +1898,6 @@ function lowerQuery(q: AnyQuery): ProgramIr {
2032
1898
  export type {
2033
1899
  AnyQuery,
2034
1900
  AnyRuleValue,
2035
- HeadFieldsOf,
2036
1901
  HeadOf,
2037
1902
  HeadShape,
2038
1903
  OutputRuleChain,