@bjornpagen/bumbledb 0.5.0 → 0.6.0

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