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