@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,38 +1,46 @@
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
  import * as errors from "@superbuilders/errors";
28
37
  import { sealedFieldsOf } from "#closed.ts";
29
38
  import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts";
30
39
  import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts";
31
- import { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, makeVar, renderFieldKind, term } from "#query/scope.ts";
32
- import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts";
40
+ import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/find.ts";
41
+ import { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, renderFieldKind, term } from "#query/scope.ts";
33
42
  /** The frozen constructor vocabulary every rule builder spreads. */
34
43
  const termOps = Object.freeze({
35
- var: makeVar,
36
44
  param: makeParam,
37
45
  inSet: makeSetParam,
38
46
  maskParam: makeMaskParam,
@@ -60,23 +68,28 @@ const termOps = Object.freeze({
60
68
  /** The empty rule state. */
61
69
  const EMPTY_RULE = Object.freeze({
62
70
  items: Object.freeze([]),
63
- varFields: Object.freeze({}),
71
+ bound: new Set(),
64
72
  paramUses: Object.freeze([])
65
73
  });
74
+ /**
75
+ * The MINT slot of a variable, the runtime twin of {@link MintSlotOf}: (i)
76
+ * verifies the mint owner is the schema's own member value — a variable
77
+ * minted from a foreign relation is refused, naming its label — and (ii)
78
+ * returns the descriptor it was minted at plus the law-computed class read
79
+ * off the schema's frozen class map. Because {@link fieldJoins} is an
80
+ * equality, judging every binding position against this one slot makes all
81
+ * cross-binding joins mutually class-equal by transitivity.
82
+ */
83
+ function mintSlotOf(context, ref) {
84
+ if (context.theory.relations[ref.owner.name] !== ref.owner) {
85
+ throw errors.new(`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`);
86
+ }
87
+ return { field: ref.field, class: context.classes[ref.owner.name]?.[ref.column] };
88
+ }
66
89
  /**
67
90
  * Judges one membership ARRAY at a binding position — legal exactly at a
68
- * CLOSED-reference field (the owner ruling: ordinary u64/str membership is
69
- * spelled through `r.inSet` params; literal arrays are the closed
70
- * vocabulary's spelling), holding ≥ 2 DISTINCT handle names (the
71
- * degenerate sets are refusals: empty selects nothing, one element is the
72
- * bare literal respelled, and a duplicate member is the same respelling in
73
- * disguise — write each member once). The returned name is
74
- * CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
75
- * so two spellings of one set, reordered or not, share one dense
76
- * `ParamId`); the members are shape-checked strings here and
77
- * roster-verified at the one verification point (`taggedHandleId`) when
78
- * the SDK supplies the set at execute — the same moment a bound `r.inSet`
79
- * param's members are judged.
91
+ * CLOSED-reference field, holding 2 DISTINCT handle names. The returned
92
+ * name is CONTENT-ADDRESSED (vocabulary + the member SET).
80
93
  */
81
94
  function membershipSet(context, field, value) {
82
95
  const roster = rosterOf(field);
@@ -104,19 +117,18 @@ function membershipSet(context, field, value) {
104
117
  return { name: `∈ ${roster.name} ${JSON.stringify(key)}`, members: Object.freeze(members) };
105
118
  }
106
119
  /**
107
- * Resolves a bindings record against an atom owner's matchable fields (a
108
- * relation's declared fields; a closed relation's sealed id + columns), in
120
+ * Resolves a bindings record against an atom owner's matchable fields, in
109
121
  * the record's written order: terms classify by their runtime tag,
110
- * everything else is a bare literal (typed by the FIELD at lowering — the
111
- * membership typing rule included). Every bound field carries its
112
- * law-computed class, read off the schema value's frozen class map — the
113
- * runtime twin of the type tier's `SlotAt` lookups.
122
+ * everything else is a bare literal. Every VARIABLE binding judges
123
+ * `fieldJoins(mintSlot, positionSlot)` and throws on a class-unequal reuse
124
+ * (the runtime twin of `CheckBindings`); the bound refs are collected for
125
+ * the rule's boundness set.
114
126
  */
115
- function resolveBindings(context, relation, bindings, classes) {
127
+ function resolveBindings(context, label, relation, bindings) {
116
128
  const entries = [];
117
129
  const vars = [];
118
130
  const uses = [];
119
- const relationClasses = classes[relation.name];
131
+ const relationClasses = context.classes[relation.name];
120
132
  const ordered = sealedFieldsOf(relation);
121
133
  for (const [fieldName, value] of Object.entries(bindings)) {
122
134
  if (value === undefined) {
@@ -126,15 +138,21 @@ function resolveBindings(context, relation, bindings, classes) {
126
138
  return candidate.name === fieldName;
127
139
  });
128
140
  if (declared === undefined) {
129
- throw errors.new(`${context} has no field ${fieldName}`);
141
+ throw errors.new(`${label} has no field ${fieldName}`);
130
142
  }
131
143
  const fieldClass = relationClasses?.[fieldName];
132
144
  let bound;
133
145
  if (isTerm(value)) {
134
146
  switch (value[term]) {
135
147
  case "var": {
136
- bound = Object.freeze({ kind: "var", name: value.name });
137
- vars.push(Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) }));
148
+ const ref = value;
149
+ const mint = mintSlotOf(context, ref);
150
+ const positionSlot = { field: declared.field, class: fieldClass };
151
+ if (!fieldJoins(mint, positionSlot)) {
152
+ throw errors.new(`${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)`);
153
+ }
154
+ bound = Object.freeze({ kind: "var", ref });
155
+ vars.push(ref);
138
156
  break;
139
157
  }
140
158
  case "param": {
@@ -160,13 +178,13 @@ function resolveBindings(context, relation, bindings, classes) {
160
178
  break;
161
179
  }
162
180
  case "maskParam":
163
- throw errors.new(`${context}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`);
181
+ throw errors.new(`${label}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`);
164
182
  case "duration":
165
- throw errors.new(`${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`);
183
+ throw errors.new(`${label}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and find entries`);
166
184
  }
167
185
  }
168
186
  else if (Array.isArray(value)) {
169
- const set = membershipSet(`${context}.${fieldName}`, declared.field, value);
187
+ const set = membershipSet(`${label}.${fieldName}`, declared.field, value);
170
188
  bound = Object.freeze({ kind: "literalSet", name: set.name, members: set.members });
171
189
  uses.push(Object.freeze({
172
190
  name: set.name,
@@ -181,49 +199,33 @@ function resolveBindings(context, relation, bindings, classes) {
181
199
  }
182
200
  entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }));
183
201
  }
184
- return {
185
- atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
186
- vars,
187
- uses
188
- };
202
+ return { atom: Object.freeze({ relation, bindings: Object.freeze(entries) }), vars, uses };
189
203
  }
190
- /**
191
- * Extends a rule state with one positive atom. Vars bind on first
192
- * occurrence; every LATER occurrence (a later atom's field or a same-record
193
- * sibling) is a join and must be class-equal — the construction-time twin
194
- * of the type tier's `JoinOk` (bare pairs only with bare), so the domain
195
- * wall holds for untyped callers too.
196
- */
197
- function advanceMatch(state, relation, bindings, classes) {
198
- const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes);
199
- const varFields = { ...state.varFields };
200
- for (const bound of resolved.vars) {
201
- const existing = varFields[bound.name];
202
- if (existing === undefined) {
203
- varFields[bound.name] = bound.slot;
204
- }
205
- else if (!fieldJoins(existing, bound.slot)) {
206
- throw errors.new(`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)`);
207
- }
204
+ /** Extends a rule state with one positive atom; the bound variable references accumulate into the boundness set. */
205
+ function advanceMatch(context, state, relation, bindings) {
206
+ const resolved = resolveBindings(context, `relation ${relation.name}`, relation, bindings);
207
+ const bound = new Set(state.bound);
208
+ for (const ref of resolved.vars) {
209
+ bound.add(ref);
208
210
  }
209
- return {
211
+ return Object.freeze({
210
212
  items: Object.freeze([...state.items, Object.freeze({ kind: "atom", atom: resolved.atom })]),
211
- varFields: Object.freeze(varFields),
213
+ bound,
212
214
  paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
213
- };
215
+ });
214
216
  }
215
- /** Resolves one comparison side to its runtime term. */
217
+ /** Resolves one comparison side to its runtime term (variables and the measure ride by reference). */
216
218
  function cmpTermDataOf(op, value) {
217
219
  if (isTerm(value)) {
218
220
  switch (value[term]) {
219
221
  case "var":
220
- return Object.freeze({ kind: "var", name: value.name });
222
+ return Object.freeze({ kind: "var", ref: value });
221
223
  case "param":
222
224
  return Object.freeze({ kind: "param", name: value.name });
223
225
  case "setParam":
224
226
  return Object.freeze({ kind: "setParam", name: value.name });
225
227
  case "duration":
226
- return Object.freeze({ kind: "measure", name: value.name });
228
+ return Object.freeze({ kind: "measure", ref: value.over });
227
229
  case "maskParam":
228
230
  throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`);
229
231
  }
@@ -232,17 +234,16 @@ function cmpTermDataOf(op, value) {
232
234
  }
233
235
  /**
234
236
  * One comparison side's contribution to the param census: a param/set side
235
- * anchors to its SIBLING — a bound variable's field descriptor or the
236
- * measure; an unanchorable use (literal or param sibling) records with no
237
- * anchor and must be anchored by some other use of the same name.
237
+ * anchors to its SIBLING — a variable's field descriptor or the measure; an
238
+ * unanchorable use records with no anchor.
238
239
  */
239
- function sideUses(op, side, sibling, varFields, uses) {
240
+ function sideUses(op, side, sibling, uses) {
240
241
  if (side.kind !== "param" && side.kind !== "setParam") {
241
242
  return;
242
243
  }
243
244
  let anchor;
244
245
  if (sibling.kind === "var") {
245
- anchor = varFields[sibling.name]?.field;
246
+ anchor = sibling.ref.field;
246
247
  }
247
248
  else if (sibling.kind === "measure") {
248
249
  anchor = "measure";
@@ -259,12 +260,12 @@ function sideUses(op, side, sibling, varFields, uses) {
259
260
  }));
260
261
  }
261
262
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
262
- function condDataOf(cond, varFields, uses) {
263
+ function condDataOf(cond, uses) {
263
264
  if (cond.cond === "cmp") {
264
265
  const lhs = cmpTermDataOf(cond.op, cond.lhs);
265
266
  const rhs = cmpTermDataOf(cond.op, cond.rhs);
266
- sideUses(cond.op, lhs, rhs, varFields, uses);
267
- sideUses(cond.op, rhs, lhs, varFields, uses);
267
+ sideUses(cond.op, lhs, rhs, uses);
268
+ sideUses(cond.op, rhs, lhs, uses);
268
269
  let mask;
269
270
  if (cond.op === "allen") {
270
271
  const maskValue = cond.mask;
@@ -285,20 +286,18 @@ function condDataOf(cond, varFields, uses) {
285
286
  throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam");
286
287
  }
287
288
  }
288
- const data = Object.freeze({ kind: "cmp", op: cond.op, mask, lhs, rhs });
289
- return data;
289
+ return Object.freeze({ kind: "cmp", op: cond.op, mask, lhs, rhs });
290
290
  }
291
291
  if (cond.cond === "tree") {
292
292
  const children = cond.children.map(function lowerChild(child) {
293
- return condDataOf(child, varFields, uses);
293
+ return condDataOf(child, uses);
294
294
  });
295
- const data = Object.freeze({ kind: "tree", op: cond.op, children: Object.freeze(children) });
296
- return data;
295
+ return Object.freeze({ kind: "tree", op: cond.op, children: Object.freeze(children) });
297
296
  }
298
297
  throw errors.new("a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()");
299
298
  }
300
299
  /** Extends a rule state with one `.where` item (a condition or a negated atom). */
301
- function advanceWhere(state, cond, classes) {
300
+ function advanceWhere(context, state, cond) {
302
301
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
303
302
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom");
304
303
  }
@@ -307,191 +306,177 @@ function advanceWhere(state, cond, classes) {
307
306
  const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
308
307
  return value !== undefined;
309
308
  }));
310
- const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes);
311
- return {
309
+ const resolved = resolveBindings(context, `negated relation ${relation.name}`, relation, bindings);
310
+ return Object.freeze({
312
311
  items: Object.freeze([...state.items, Object.freeze({ kind: "negated", atom: resolved.atom })]),
313
- varFields: state.varFields,
312
+ bound: state.bound,
314
313
  paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
315
- };
314
+ });
316
315
  }
317
316
  const uses = [];
318
- const data = condDataOf(cond, state.varFields, uses);
319
- return {
317
+ const data = condDataOf(cond, uses);
318
+ return Object.freeze({
320
319
  items: Object.freeze([...state.items, Object.freeze({ kind: "cond", cond: data })]),
321
- varFields: state.varFields,
320
+ bound: state.bound,
322
321
  paramUses: Object.freeze([...state.paramUses, ...uses])
323
- };
322
+ });
324
323
  }
325
- /** Extends a rule state with one `idb` atom (vars must be bound validated at completion). */
326
- function advanceIdb(state, rec, vars) {
327
- const names = vars.map(function nameOf(variable) {
328
- if (!isTerm(variable) || variable[term] !== "var") {
329
- throw errors.new(`idb ${rec.name}: positions take variables — bind literals and params through where()/match()`);
324
+ /** Extends a rule state with one `idb` atom (a named record over head keys; vars validated at completion). */
325
+ function advanceIdb(state, rec, bindings) {
326
+ const resolved = [];
327
+ for (const [key, value] of Object.entries(bindings)) {
328
+ if (value === undefined) {
329
+ continue;
330
330
  }
331
- return variable.name;
332
- });
333
- return {
334
- items: Object.freeze([...state.items, Object.freeze({ kind: "idb", rec, vars: Object.freeze(names) })]),
335
- varFields: state.varFields,
331
+ if (!isTerm(value) || value[term] !== "var") {
332
+ throw errors.new(`idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`);
333
+ }
334
+ resolved.push(Object.freeze({ key, ref: value }));
335
+ }
336
+ return Object.freeze({
337
+ items: Object.freeze([
338
+ ...state.items,
339
+ Object.freeze({ kind: "idb", rec, bindings: Object.freeze(resolved) })
340
+ ]),
341
+ bound: state.bound,
336
342
  paramUses: state.paramUses
337
- };
343
+ });
338
344
  }
339
- /** Narrows a select entry to an aggregate value. */
345
+ /** Narrows a find entry to an aggregate value. */
340
346
  function isAggregateEntry(value) {
341
347
  return typeof value === "object" && value !== null && "agg" in value;
342
348
  }
343
- /**
344
- * Classifies one select entry into its named answer column. The `closed`
345
- * slice is resolved LATER, at rule completion (`completeRule`), where the
346
- * rule's `varFields` are in hand — until then every column is provisionally
347
- * bare.
348
- */
349
- function selectColumnOf(entry) {
350
- if (typeof entry === "string") {
351
- return Object.freeze({
352
- name: entry,
353
- entry: Object.freeze({ kind: "var", over: entry }),
354
- closed: undefined
355
- });
356
- }
357
- if (isTerm(entry)) {
358
- if (entry[term] === "duration") {
359
- return Object.freeze({
360
- name: entry.name,
361
- entry: Object.freeze({ kind: "measure", over: entry.name }),
362
- closed: undefined
363
- });
364
- }
365
- throw errors.new(`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`);
366
- }
367
- if (isAggregateEntry(entry)) {
368
- return aggregateColumnOf(entry);
349
+ /** Narrows a value to a variable term, else a pointed refusal. */
350
+ function asVarTerm(context, value) {
351
+ if (isTerm(value) && value[term] === "var") {
352
+ return value;
369
353
  }
370
- throw errors.new("query select: not a select entry — select takes variable names, duration(v), or aggregates");
354
+ throw errors.new(`${context}: expected a variable`);
371
355
  }
372
- /** Classifies one aggregate select entry. */
373
- function aggregateColumnOf(entry) {
374
- function column(name, agg) {
375
- return Object.freeze({
376
- name,
377
- entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }),
378
- closed: undefined
379
- });
380
- }
356
+ /** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
357
+ function aggDataOf(name, entry) {
381
358
  const over = entry.over;
382
359
  switch (entry.agg) {
383
360
  case "count":
384
- return column("count", { op: "count" });
385
- case "countDistinct": {
386
- if (typeof over !== "string") {
387
- throw errors.new("countDistinct takes a variable name");
388
- }
389
- return column(over, { op: "countDistinct", over });
390
- }
361
+ return Object.freeze({ op: "count" });
362
+ case "countDistinct":
363
+ return Object.freeze({ op: "countDistinct", over: asVarTerm(`find ${name} (countDistinct)`, over) });
391
364
  case "sum":
392
365
  case "min":
393
366
  case "max": {
394
- if (typeof over === "string") {
395
- return column(over, { op: "fold", fold: entry.agg, over });
367
+ if (isTerm(over) && over[term] === "var") {
368
+ return Object.freeze({ op: "fold", fold: entry.agg, over });
396
369
  }
397
370
  if (isTerm(over) && over[term] === "duration") {
398
- return column(over.name, { op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.name }) });
371
+ return Object.freeze({ op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.over }) });
399
372
  }
400
- throw errors.new(`${entry.agg} takes a variable name or duration(v)`);
373
+ throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`);
401
374
  }
402
375
  case "argMax":
403
- case "argMin": {
404
- if (typeof over !== "string" || typeof entry.key !== "string") {
405
- throw errors.new(`${entry.agg} takes a carried variable name and an orderable key variable name`);
406
- }
407
- return column(over, { op: "arg", direction: entry.agg, over, key: entry.key });
376
+ case "argMin":
377
+ return Object.freeze({
378
+ op: "arg",
379
+ direction: entry.agg,
380
+ over: asVarTerm(`find ${name} (${entry.agg})`, over),
381
+ key: asVarTerm(`find ${name} (${entry.agg} key)`, entry.key)
382
+ });
383
+ case "pack":
384
+ return Object.freeze({ op: "pack", over: asVarTerm(`find ${name} (pack)`, over) });
385
+ default:
386
+ throw errors.new(`find ${name}: unknown aggregate ${entry.agg}`);
387
+ }
388
+ }
389
+ /**
390
+ * Classifies one find entry into its named answer column (the KEY names the
391
+ * column, `count` included). The `slot`/`closed` slices are resolved LATER,
392
+ * at rule completion, where boundness and the mint slots are in hand.
393
+ */
394
+ function findColumnOf(name, entry) {
395
+ if (isTerm(entry)) {
396
+ if (entry[term] === "var") {
397
+ return Object.freeze({
398
+ name,
399
+ entry: Object.freeze({ kind: "var", over: entry }),
400
+ closed: undefined,
401
+ slot: undefined
402
+ });
408
403
  }
409
- case "pack": {
410
- if (typeof over !== "string") {
411
- throw errors.new("pack takes a variable name");
412
- }
413
- return column(over, { op: "pack", over });
404
+ if (entry[term] === "duration") {
405
+ return Object.freeze({
406
+ name,
407
+ entry: Object.freeze({ kind: "measure", over: entry.over }),
408
+ closed: undefined,
409
+ slot: undefined
410
+ });
414
411
  }
415
- default:
416
- throw errors.new(`unknown aggregate ${entry.agg}`);
412
+ throw errors.new(`find ${name}: a ${entry[term]} is not projectable — find takes variables, r.duration(v), or aggregates`);
413
+ }
414
+ if (isAggregateEntry(entry)) {
415
+ return Object.freeze({
416
+ name,
417
+ entry: Object.freeze({ kind: "aggregate", agg: aggDataOf(name, entry) }),
418
+ closed: undefined,
419
+ slot: undefined
420
+ });
417
421
  }
422
+ throw errors.new(`find ${name}: not a find entry — find takes variables, r.duration(v), or aggregates`);
418
423
  }
419
424
  /**
420
425
  * The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
421
- * § orderability): a closed reference is equality-and-membership only
422
- * its declaration-id order is an encoding accident, so every
423
- * order-comparison and fold position refuses it. The construction-time
424
- * twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
425
- * untyped callers too (the engine cannot backstop this one: the wire IR
426
- * carries plain u64s, no rosters).
426
+ * § orderability): a closed reference is equality-and-membership only.
427
427
  */
428
428
  function closedOrderError(context, position, vocabulary) {
429
429
  return errors.new(`${context}: ${position} is a ${vocabulary} reference — declaration order is an accident, not semantics: vocabularies do not order (docs/architecture/10-data-model.md; equality, membership, and counting remain)`);
430
430
  }
431
- /** The comparison ops under the orderable ban (order roster + point membership — every order-comparison position). */
431
+ /** The comparison ops under the orderable ban (order roster + point membership). */
432
432
  function isOrderOp(op) {
433
433
  return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn";
434
434
  }
435
- /** Requires a var name to be bound by a relation atom of the rule. */
436
- function assertBound(context, varFields, name) {
437
- const slot = varFields[name];
438
- if (slot === undefined) {
439
- throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`);
435
+ /** Requires a variable to be bound by a relation atom of the rule (the boundness wall — invisible to the type tier). */
436
+ function assertBound(where, bound, ref) {
437
+ if (!bound.has(ref)) {
438
+ throw errors.new(`${where}: the variable ${ref.label} is not bound by a relation atom of the rule`);
440
439
  }
441
- return slot;
442
440
  }
443
- /** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
444
- function assertIntervalBound(context, varFields, name) {
445
- const slot = assertBound(context, varFields, name);
446
- if (slot.field.kind !== "interval") {
447
- throw errors.new(`${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`);
441
+ /** Requires a variable to be interval-typed (the measure's and pack's domain), off its own descriptor. */
442
+ function assertInterval(where, ref) {
443
+ if (ref.field.kind !== "interval") {
444
+ throw errors.new(`${where}: ${ref.label} is not interval-typed — the measure is defined over interval-typed variables only`);
445
+ }
446
+ }
447
+ /** Requires a variable's own field to be non-closed (the orderable ban's runtime twin). */
448
+ function assertNotClosed(where, position, ref) {
449
+ const roster = rosterOf(ref.field);
450
+ if (roster !== undefined) {
451
+ throw closedOrderError(where, `${position} ${ref.label}`, roster.name);
448
452
  }
449
453
  }
450
454
  /**
451
- * Validates one condition's variable references against the rule's bound
452
- * names and, for `eq`/`ne` over two variables, holds the class wall: the
453
- * unification IS a join, so the two slots must be class-equal exactly as a
454
- * match-reuse join must be (the construction-time twin of the type tier's
455
- * `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
456
- * this one — the query IR carries no domains — so the wall lives here for
457
- * untyped callers too.
455
+ * The classed mint slot one answer column's VALUES flow from: a projected
456
+ * variable's mint slot, or an Arg-carried payload's. Counts, folds, `pack`
457
+ * and the measure derive numbers/intervals, so they resolve no slot.
458
458
  */
459
- function validateCond(context, varFields, cond) {
460
- if (cond.kind === "cmp") {
461
- for (const side of [cond.lhs, cond.rhs]) {
462
- if (side.kind === "var") {
463
- const slot = assertBound(context, varFields, side.name);
464
- const roster = rosterOf(slot.field);
465
- if (isOrderOp(cond.op) && roster !== undefined) {
466
- throw closedOrderError(context, `the ${cond.op} side ${side.name}`, roster.name);
467
- }
468
- }
469
- if (side.kind === "measure") {
470
- assertIntervalBound(context, varFields, side.name);
471
- }
472
- }
473
- if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
474
- const lhs = assertBound(context, varFields, cond.lhs.name);
475
- const rhs = assertBound(context, varFields, cond.rhs.name);
476
- if (!fieldJoins(lhs, rhs)) {
477
- throw errors.new(`${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)`);
478
- }
479
- }
480
- return;
459
+ function findColumnSlotOf(context, column) {
460
+ const entry = column.entry;
461
+ if (entry.kind === "var") {
462
+ return mintSlotOf(context, entry.over);
481
463
  }
482
- for (const child of cond.children) {
483
- validateCond(context, varFields, child);
464
+ if (entry.kind === "aggregate" && entry.agg.op === "arg") {
465
+ return mintSlotOf(context, entry.agg.over);
484
466
  }
467
+ return undefined;
485
468
  }
486
- /** Validates one select column's variable references. */
487
- function validateColumn(context, varFields, column) {
469
+ /** Validates one find column's variable references (boundness + the orderable/interval walls, off the var's own field). */
470
+ function validateColumn(context, bound, column) {
471
+ const where = `${contextLabel(context)} find ${column.name}`;
488
472
  const entry = column.entry;
489
473
  if (entry.kind === "var") {
490
- assertBound(`${context} select ${column.name}`, varFields, entry.over);
474
+ assertBound(where, bound, entry.over);
491
475
  return;
492
476
  }
493
477
  if (entry.kind === "measure") {
494
- assertIntervalBound(`${context} select ${column.name}`, varFields, entry.over);
478
+ assertBound(where, bound, entry.over);
479
+ assertInterval(where, entry.over);
495
480
  return;
496
481
  }
497
482
  const agg = entry.agg;
@@ -499,128 +484,143 @@ function validateColumn(context, varFields, column) {
499
484
  case "count":
500
485
  return;
501
486
  case "countDistinct":
502
- assertBound(`${context} select ${column.name}`, varFields, agg.over);
487
+ assertBound(where, bound, agg.over);
503
488
  return;
504
489
  case "fold": {
505
- if (typeof agg.over === "string") {
506
- const slot = assertBound(`${context} select ${column.name}`, varFields, agg.over);
507
- const roster = rosterOf(slot.field);
508
- if (roster !== undefined) {
509
- throw closedOrderError(`${context} select ${column.name}`, `the ${agg.fold} input ${agg.over}`, roster.name);
510
- }
490
+ if ("duration" in agg.over) {
491
+ assertBound(where, bound, agg.over.duration);
492
+ assertInterval(where, agg.over.duration);
511
493
  return;
512
494
  }
513
- assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration);
495
+ assertBound(where, bound, agg.over);
496
+ assertNotClosed(where, `the ${agg.fold} input`, agg.over);
514
497
  return;
515
498
  }
516
499
  case "arg": {
517
- assertBound(`${context} select ${column.name}`, varFields, agg.over);
518
- const key = assertBound(`${context} select ${column.name}`, varFields, agg.key);
519
- const keyRoster = rosterOf(key.field);
520
- if (keyRoster !== undefined) {
521
- throw closedOrderError(`${context} select ${column.name}`, `the ${agg.direction} key ${agg.key}`, keyRoster.name);
522
- }
500
+ assertBound(where, bound, agg.over);
501
+ assertBound(where, bound, agg.key);
502
+ assertNotClosed(where, `the ${agg.direction} key`, agg.key);
523
503
  return;
524
504
  }
525
505
  case "pack":
526
- assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over);
506
+ assertBound(where, bound, agg.over);
507
+ assertInterval(where, agg.over);
527
508
  return;
528
509
  }
529
510
  }
530
511
  /**
531
- * Resolves the roster one select column decodes through: a projected var,
532
- * or an Arg-carried payload, bound at a closed-referencing field carries
533
- * that field's roster (read off `varFields` the same slot the domain
534
- * machinery reads), and `decodeAnswers` lifts the column's row ids back to
535
- * handle NAMES through it — the runtime twin of the row type's `Infer`
536
- * claim. Every other entry decodes bare: counts are counts, the measure
537
- * and `pack` are never closed, and a closed FOLD is banned outright
538
- * ({@link closedOrderError}) before this resolution runs.
512
+ * Validates one condition's variable references against the rule's bound
513
+ * set and, for `eq`/`ne` over two variables, holds the class wall through
514
+ * the mint slots (the unification IS a join; bare pairs only with bare).
539
515
  */
540
- function selectClosedOf(varFields, entry) {
541
- let over;
542
- if (entry.kind === "var") {
543
- over = entry.over;
544
- }
545
- else if (entry.kind === "aggregate" && entry.agg.op === "arg") {
546
- over = entry.agg.over;
547
- }
548
- else {
549
- over = undefined;
516
+ function validateCond(context, bound, cond) {
517
+ const label = contextLabel(context);
518
+ if (cond.kind === "cmp") {
519
+ for (const side of [cond.lhs, cond.rhs]) {
520
+ if (side.kind === "var") {
521
+ assertBound(label, bound, side.ref);
522
+ const roster = rosterOf(side.ref.field);
523
+ if (isOrderOp(cond.op) && roster !== undefined) {
524
+ throw closedOrderError(label, `the ${cond.op} side ${side.ref.label}`, roster.name);
525
+ }
526
+ }
527
+ if (side.kind === "measure") {
528
+ assertBound(label, bound, side.ref);
529
+ assertInterval(label, side.ref);
530
+ }
531
+ }
532
+ if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
533
+ assertBound(label, bound, cond.lhs.ref);
534
+ assertBound(label, bound, cond.rhs.ref);
535
+ const lhs = mintSlotOf(context, cond.lhs.ref);
536
+ const rhs = mintSlotOf(context, cond.rhs.ref);
537
+ if (!fieldJoins(lhs, rhs)) {
538
+ throw errors.new(`${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)`);
539
+ }
540
+ }
541
+ return;
550
542
  }
551
- if (over === undefined) {
552
- return undefined;
543
+ for (const child of cond.children) {
544
+ validateCond(context, bound, child);
553
545
  }
554
- return rosterOf(varFields[over]?.field);
555
546
  }
556
547
  /**
557
- * Completes one rule: classifies the select record (written order = answer
558
- * column order, names must be declaration-order-safe keys), and validates
559
- * boundness — every condition/select/idb variable bound by a relation atom,
560
- * and every NEGATED atom's variable positively bound (the safety rule: a
561
- * negated atom binds nothing, only rejects).
548
+ * Validates one `idb` item: every head column of the rec is bound exactly
549
+ * once (a missing or extra key is a pointed error), every bound variable is
550
+ * positively bound by a relation atom of the rule, and each variable joins
551
+ * its head column's classed slot. When the rec's own rule 0 is in flight
552
+ * (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
553
+ * head.
562
554
  */
563
- function completeRule(context, state, columns) {
564
- if (columns.length === 0) {
565
- throw errors.new(`${context}: a select needs at least one entry`);
555
+ function validateIdb(context, bound, item, columns) {
556
+ const label = contextLabel(context);
557
+ const head = item.rec.rules[0];
558
+ const headColumns = head !== undefined ? head.finds : columns;
559
+ const headNames = headColumns.map(function nameOf(column) {
560
+ return column.name;
561
+ });
562
+ const keys = item.bindings.map(function keyOf(binding) {
563
+ return binding.key;
564
+ });
565
+ for (const key of keys) {
566
+ if (!headNames.includes(key)) {
567
+ throw errors.new(`${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`);
568
+ }
566
569
  }
567
- const seen = new Set();
568
- for (const column of columns) {
569
- assertDeclarationOrderKey(`${context} select column`, column.name);
570
- if (seen.has(column.name)) {
571
- throw errors.new(`${context}: select names the answer column ${column.name} twice`);
570
+ for (const name of headNames) {
571
+ if (!keys.includes(name)) {
572
+ throw errors.new(`${label}: idb ${item.rec.name} omits the head column ${name} — an idb join binds every head column of ${item.rec.name}`);
572
573
  }
573
- seen.add(column.name);
574
- validateColumn(context, state.varFields, column);
575
574
  }
575
+ for (const binding of item.bindings) {
576
+ if (!bound.has(binding.ref)) {
577
+ throw errors.new(`${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`);
578
+ }
579
+ const headColumn = headColumns.find(function byName(column) {
580
+ return column.name === binding.key;
581
+ });
582
+ if (headColumn === undefined || headColumn.slot === undefined) {
583
+ continue;
584
+ }
585
+ const mint = mintSlotOf(context, binding.ref);
586
+ if (!fieldJoins(headColumn.slot, mint)) {
587
+ throw errors.new(`${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`);
588
+ }
589
+ }
590
+ }
591
+ /**
592
+ * Completes one rule: enriches the find columns (declaration-order-safe
593
+ * keys, boundness validated, each column's classed slot and closed slice
594
+ * resolved), then walks the body walls — negated-atom boundness safety, idb
595
+ * head pairing, and condition validation.
596
+ */
597
+ function completeRule(context, state, rawColumns) {
598
+ const label = contextLabel(context);
599
+ if (rawColumns.length === 0) {
600
+ throw errors.new(`${label}: a find needs at least one entry`);
601
+ }
602
+ const columns = rawColumns.map(function enrichColumn(column) {
603
+ assertDeclarationOrderKey(`${label} find column`, column.name);
604
+ validateColumn(context, state.bound, column);
605
+ const slot = findColumnSlotOf(context, column);
606
+ return Object.freeze({ name: column.name, entry: column.entry, slot, closed: rosterOf(slot?.field) });
607
+ });
576
608
  for (const item of state.items) {
577
609
  if (item.kind === "negated") {
578
610
  for (const binding of item.atom.bindings) {
579
- if (binding.term.kind === "var") {
580
- const bound = state.varFields[binding.term.name];
581
- if (bound === undefined) {
582
- throw errors.new(`${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)`);
583
- }
584
- const negatedSlot = { field: binding.data, class: binding.class };
585
- if (!fieldJoins(bound, negatedSlot)) {
586
- throw errors.new(`${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`);
587
- }
611
+ if (binding.term.kind === "var" && !state.bound.has(binding.term.ref)) {
612
+ throw errors.new(`${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)`);
588
613
  }
589
614
  }
590
615
  }
591
616
  if (item.kind === "idb") {
592
- const head = item.rec.rules[0];
593
- item.vars.forEach(function checkIdbVar(name, position) {
594
- const bound = state.varFields[name];
595
- if (bound === undefined) {
596
- throw errors.new(`${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`);
597
- }
598
- const column = head?.select[position];
599
- if (column === undefined || column.entry.kind !== "var") {
600
- return;
601
- }
602
- const headSlot = head?.varFields[column.entry.over];
603
- if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
604
- throw errors.new(`${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`);
605
- }
606
- });
617
+ validateIdb(context, state.bound, item, columns);
607
618
  }
608
619
  if (item.kind === "cond") {
609
- validateCond(context, state.varFields, item.cond);
620
+ validateCond(context, state.bound, item.cond);
610
621
  }
611
622
  }
612
- return Object.freeze({
613
- items: state.items,
614
- select: Object.freeze(columns.map(function enrichColumn(column) {
615
- return Object.freeze({
616
- name: column.name,
617
- entry: column.entry,
618
- closed: selectClosedOf(state.varFields, column.entry)
619
- });
620
- })),
621
- varFields: state.varFields,
622
- paramUses: state.paramUses
623
- });
623
+ return Object.freeze({ items: state.items, finds: Object.freeze(columns), paramUses: state.paramUses });
624
624
  }
625
625
  /** Builds one typed rule value over completed rule data. */
626
626
  function makeRuleValue(rule) {
@@ -638,7 +638,7 @@ function contextLabel(context) {
638
638
  }
639
639
  }
640
640
  /** Validates and records one `idb` atom per the context's cut. */
641
- function idbAdvance(context, state, target, vars) {
641
+ function idbAdvance(context, state, target, bindings) {
642
642
  if (context.kind === "query") {
643
643
  throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
644
644
  }
@@ -646,36 +646,41 @@ function idbAdvance(context, state, target, vars) {
646
646
  if (target.data !== context.self) {
647
647
  throw errors.new(`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)`);
648
648
  }
649
- return advanceIdb(state, context.self, vars);
649
+ return advanceIdb(state, context.self, bindings);
650
650
  }
651
651
  if (!context.program.recs.includes(target.data)) {
652
652
  throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
653
653
  }
654
- return advanceIdb(state, target.data, vars);
654
+ return advanceIdb(state, target.data, bindings);
655
655
  }
656
- /** Classifies one select tuple per the context (a recursive head projects bound NAMES only). */
657
- function selectColumns(context, entries) {
658
- return entries.map(function columnOf(entry) {
659
- if (context.kind === "rec" && typeof entry !== "string") {
660
- throw errors.new(`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)`);
656
+ /** Classifies one find record per the context (a recursive head projects bound variables only). */
657
+ function findColumns(context, entries) {
658
+ const columns = [];
659
+ for (const [name, entry] of Object.entries(entries)) {
660
+ if (entry === undefined) {
661
+ continue;
661
662
  }
662
- return selectColumnOf(entry);
663
- });
663
+ if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
664
+ throw errors.new(`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)`);
665
+ }
666
+ columns.push(findColumnOf(name, entry));
667
+ }
668
+ return columns;
664
669
  }
665
670
  /** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
666
671
  function makeRawChain(context, state) {
667
672
  const chain = {
668
673
  match(relation, bindings) {
669
- return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes));
674
+ return makeRawChain(context, advanceMatch(context, state, relation, bindings));
670
675
  },
671
676
  where(cond) {
672
- return makeRawChain(context, advanceWhere(state, cond, context.classes));
677
+ return makeRawChain(context, advanceWhere(context, state, cond));
673
678
  },
674
- idb(target, ...vars) {
675
- return makeRawChain(context, idbAdvance(context, state, target, vars));
679
+ idb(target, bindings) {
680
+ return makeRawChain(context, idbAdvance(context, state, target, bindings));
676
681
  },
677
- select(...entries) {
678
- return makeRuleValue(completeRule(contextLabel(context), state, selectColumns(context, entries)));
682
+ find(entries) {
683
+ return makeRuleValue(completeRule(context, state, findColumns(context, entries)));
679
684
  }
680
685
  };
681
686
  Object.freeze(chain);
@@ -686,7 +691,7 @@ function makeRawScope(context) {
686
691
  const scope = {
687
692
  ...termOps,
688
693
  match(relation, bindings) {
689
- return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes));
694
+ return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings));
690
695
  }
691
696
  };
692
697
  Object.freeze(scope);
@@ -694,21 +699,19 @@ function makeRawScope(context) {
694
699
  }
695
700
  /**
696
701
  * The rule builders' trusted admission seam — THE home of the
697
- * trusted-admission-seam pattern the other mint guards cite (the face,
698
- * class-map, axiom-readback, rec-handle, and query-value seams): the raw
699
- * builder is one runtime shape for every context, and this guard verifies
700
- * the checkable fact the builder verbs exist — before the value is
701
- * admitted at its TYPED face. The type-level
702
- * judgments (domain-equal joins, boundness, the recursion cut) live in the
703
- * interfaces themselves; the runtime twin of every one of them is a
704
- * construction-time validation in this module.
702
+ * trusted-admission-seam pattern the other mint guards cite: the raw builder
703
+ * is one runtime shape for every context, and this guard verifies the
704
+ * checkable fact the builder verbs exist before the value is admitted at
705
+ * its TYPED face. The type-level judgments (class-equal joins, the recursion
706
+ * cut) live in the interfaces themselves; boundness is a construction-time
707
+ * validation in this module (object identity is invisible to the type tier).
705
708
  */
706
709
  function isTypedScope(scope) {
707
710
  return typeof scope.match === "function";
708
711
  }
709
712
  /** Builds one query-rule builder (the typed face of the raw builder). */
710
- function makeQueryRuleScope(classes) {
711
- const raw = makeRawScope({ kind: "query", classes });
713
+ function makeQueryRuleScope(theory) {
714
+ const raw = makeRawScope({ kind: "query", classes: theory.classes, theory });
712
715
  if (!isTypedScope(raw)) {
713
716
  throw errors.new("query rule builder construction incomplete");
714
717
  }
@@ -716,7 +719,7 @@ function makeQueryRuleScope(classes) {
716
719
  }
717
720
  /** Builds one output-rule builder over a program's recs. */
718
721
  function makeOutputRuleScope(program) {
719
- const raw = makeRawScope({ kind: "output", program, classes: program.classes });
722
+ const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory });
720
723
  if (!isTypedScope(raw)) {
721
724
  throw errors.new("program output rule builder construction incomplete");
722
725
  }
@@ -741,25 +744,7 @@ function headSignature(column) {
741
744
  }
742
745
  return `${column.name}:${agg.op}`;
743
746
  }
744
- /**
745
- * The classed slot one answer column's VALUES flow from, resolved through
746
- * the rule's own binding environment: a projected var's first-binding slot,
747
- * or an Arg-carried payload's (`argMax`/`argMin` carry `over` verbatim —
748
- * the same two shapes the closed slice lifts). Counts, folds, `pack` and
749
- * the measure derive numbers/intervals rather than carrying a slot's ids,
750
- * so they resolve no slot (`undefined`).
751
- */
752
- function headSlotOf(rule, column) {
753
- const entry = column.entry;
754
- if (entry.kind === "var") {
755
- return rule.varFields[entry.over];
756
- }
757
- if (entry.kind === "aggregate" && entry.agg.op === "arg") {
758
- return rule.varFields[entry.agg.over];
759
- }
760
- return undefined;
761
- }
762
- /** The roster a param anchor carries: present exactly on a closed-reference field anchor (rides THE one `rosterOf` reader). */
747
+ /** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
763
748
  function anchorRosterOf(anchor) {
764
749
  return anchor === "measure" ? undefined : rosterOf(anchor);
765
750
  }
@@ -769,20 +754,9 @@ function renderParamAnchor(roster) {
769
754
  }
770
755
  /**
771
756
  * Folds every rule's param uses (recs in declaration order first, output
772
- * rules last — exactly the lowering walk) into the query's registry:
773
- * first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
774
- * the wire, and one name must keep one shape AND one closedness — every
775
- * anchored use of one name must agree on the roster (value identity), so a
776
- * param anchored at a closed reference is GUARANTEED to ride the one
777
- * roster-verification point (`taggedHandleId`) at execute; a name anchored
778
- * both at a closed reference and at a non-closed position (or at two
779
- * vocabularies) is refused here, because the wire would translate only the
780
- * first anchor's reading (the type tier intersects the uses to `never`;
781
- * this is its runtime twin for untyped callers). A param whose anchor is a
782
- * CLOSED reference must never sit in an order-comparison position — the
783
- * anchor types its value a handle name and the engine would order the
784
- * translated row ids, so the pairing is refused here too (the registry is
785
- * the one place a name's every use and its anchoring field meet).
757
+ * rules last — exactly the lowering walk) into the query's registry: first
758
+ * use mints the dense `ParamId`, the first FIELD-ANCHORED use types the
759
+ * wire, and one name keeps one shape AND one closedness.
786
760
  */
787
761
  function paramRegistryOf(recs, rules) {
788
762
  const order = [];
@@ -846,58 +820,52 @@ function paramRegistryOf(recs, rules) {
846
820
  /**
847
821
  * Assembles the runtime query value over completed rules: every rule must
848
822
  * derive the SAME head (name and aggregate shape, position for position —
849
- * the decode labels and the engine's alignment rule agree by
850
- * construction), and the param registry folds in program-walk order.
823
+ * the decode labels and the engine's alignment rule agree), and the param
824
+ * registry folds in program-walk order.
851
825
  */
852
826
  function makeRawQuery(theory, recs, rules) {
853
827
  const first = rules[0];
854
828
  if (first === undefined) {
855
829
  throw errors.new("a query needs at least one rule");
856
830
  }
857
- const signature = first.select.map(headSignature).join(", ");
831
+ const signature = first.finds.map(headSignature).join(", ");
858
832
  rules.forEach(function verifyHead(rule, index) {
859
- const candidate = rule.select.map(headSignature).join(", ");
833
+ const candidate = rule.finds.map(headSignature).join(", ");
860
834
  if (candidate !== signature) {
861
- throw errors.new(`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`);
835
+ throw errors.new(`every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`);
862
836
  }
863
837
  // The closed slice is part of the head too: one answer column decodes
864
- // through one roster, so a union whose rules bind a column at
865
- // different vocabularies (or one closed, one bare the ids would
866
- // mistranslate silently) is refused pointed. Vocabulary identity is
867
- // value identity, the SDK's membership rule everywhere.
868
- rule.select.forEach(function verifyClosedSlice(column, position) {
869
- const lead = first.select[position];
838
+ // through one roster, so a union whose rules bind a column at different
839
+ // vocabularies (or one closed, one bare) is refused pointed.
840
+ rule.finds.forEach(function verifyClosedSlice(column, position) {
841
+ const lead = first.finds[position];
870
842
  if (lead !== undefined && column.closed !== lead.closed) {
871
843
  throw errors.new(`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)`);
872
844
  }
873
845
  // The law-class wall on the union head: one answer column is one
874
- // value space, so the classed slot each rule binds the column at
875
- // must join across rules — the SAME fieldJoins judgment every
876
- // join/eq/negated-atom position enforces. The SDK holds this wall
877
- // because the wire IR carries no domains: the engine cannot
878
- // backstop it, and without it a union mixes (say) Holder ids and
879
- // Account ids in one column the consumer reads as one id space.
846
+ // value space, so the classed mint slot each rule binds the column
847
+ // at must join across rules — the SAME fieldJoins judgment every
848
+ // join/eq/negated-atom position enforces (the SDK holds it because
849
+ // the wire IR carries no domains).
880
850
  if (lead === undefined) {
881
851
  return;
882
852
  }
883
- const leadSlot = headSlotOf(first, lead);
884
- const slot = headSlotOf(rule, column);
885
- if (leadSlot !== undefined && slot !== undefined && !fieldJoins(leadSlot, slot)) {
886
- throw errors.new(`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)`);
853
+ if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
854
+ throw errors.new(`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)`);
887
855
  }
888
856
  });
889
857
  });
890
858
  const data = Object.freeze({
891
859
  recs: Object.freeze([...recs]),
892
860
  rules: Object.freeze([...rules]),
893
- select: first.select,
861
+ finds: first.finds,
894
862
  params: paramRegistryOf(recs, rules)
895
863
  });
896
864
  const value = {
897
865
  schema: theory,
898
866
  data,
899
867
  rule(build) {
900
- const built = build(makeRawScope({ kind: "query", classes: theory.classes }));
868
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }));
901
869
  return makeRawQuery(theory, recs, [...rules, built.rule]);
902
870
  }
903
871
  };
@@ -921,16 +889,15 @@ function makeQuery(theory, recs, rules) {
921
889
  return raw;
922
890
  }
923
891
  /**
924
- * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
925
- * adds one conjunctive rule; multiple rules are the set union (answers are
926
- * SETS no order or limit exists anywhere; hosts sort). The schema's
927
- * law-computed class map rides into every rule builder — the join walls
928
- * compare class names off it, at the type level and at construction alike.
892
+ * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
893
+ * one conjunctive rule; multiple rules are the set union. The schema's
894
+ * law-computed class map and theory value ride into every rule builder — the
895
+ * join walls compare against the mint slots off it.
929
896
  */
930
897
  function query(theory) {
931
898
  const start = {
932
899
  rule(build) {
933
- const built = build(makeQueryRuleScope(theory.classes));
900
+ const built = build(makeQueryRuleScope(theory));
934
901
  return makeQuery(theory, [], [built.rule]);
935
902
  }
936
903
  };
@@ -939,13 +906,8 @@ function query(theory) {
939
906
  }
940
907
  /**
941
908
  * Tags one closed-reference literal: the handle NAME, verified against the
942
- * roster (the belt the wide fallback type cannot provide structural
943
- * values make any string spellable here) and translated to its
944
- * declaration-order row id, tagged u64 — queries cross ids, never handle
945
- * names; the wire is untouched. THE single roster-verification point of
946
- * the query surface: atom-binding literals, comparison literals,
947
- * execute-time params, and membership-array members all reach it (never
948
- * duplicate the check per call site).
909
+ * roster and translated to its declaration-order row id, tagged u64. THE
910
+ * single roster-verification point of the query surface.
949
911
  */
950
912
  function taggedHandleId(context, closed, value) {
951
913
  if (typeof value !== "string") {
@@ -979,11 +941,7 @@ function taggedAtElementDomain(context, element, value) {
979
941
  }
980
942
  /**
981
943
  * Tags one host literal at a FIELD position (atom bindings): the field's
982
- * structural kind directs the tag, never a guess. At an interval field a
983
- * bigint literal tags as the ELEMENT type — the IR's membership typing
984
- * rule (point membership), an interval-shaped literal as the interval
985
- * (value equality). A closed-reference literal is its bare handle id,
986
- * tagged u64 after a roster verification.
944
+ * structural kind directs the tag, never a guess.
987
945
  */
988
946
  function taggedLiteral(context, field, value) {
989
947
  const roster = rosterOf(field);
@@ -1013,15 +971,6 @@ function taggedLiteral(context, field, value) {
1013
971
  if (typeof value !== "string") {
1014
972
  throw literalShapeError(context, "string", value);
1015
973
  }
1016
- /**
1017
- * The marshal's bijection law at the query seam (`marshal.ts`
1018
- * cellOf): a lone surrogate would be lossily replaced with
1019
- * U+FFFD at the bridge's UTF-8 crossing and silently match a
1020
- * fact the typed write surface can never store — distinct JS
1021
- * strings collapsing to one wire query. This is the single
1022
- * seam every query string literal, string param
1023
- * (`taggedCmpLiteral`), and membership member lowers through.
1024
- */
1025
974
  if (!value.isWellFormed()) {
1026
975
  throw literalShapeError(context, "well-formed string", value);
1027
976
  }
@@ -1038,17 +987,13 @@ function taggedLiteral(context, field, value) {
1038
987
  }
1039
988
  }
1040
989
  /**
1041
- * Tags one host literal at a COMPARISON or PARAM position, where the
1042
- * SIBLING anchors the type: a measure sibling is u64, an interval-field
1043
- * sibling contributes its element domain (so both a point literal in
1044
- * `pointIn` and a `span` literal in `allen` tag correctly), a scalar
1045
- * sibling its own type. At `pointIn` the operand order is interval-left,
1046
- * point-right (`ir::CmpOp::PointIn`), so an interval-shaped literal
1047
- * beside a scalar element-typed sibling is the LEGAL interval operand of
1048
- * `pointIn(t, span(...))` and tags as the interval of the sibling's
1049
- * element domain; under every other operator an interval shape against a
1050
- * scalar sibling stays refused (the engine's IllegalComparison — the
1051
- * bug-hunt fix, preserved op-aware).
990
+ * Tags one host literal at a COMPARISON or PARAM position, where the SIBLING
991
+ * anchors the type: a measure sibling is u64, an interval-field sibling
992
+ * contributes its element domain, a scalar sibling its own type. At
993
+ * `pointIn` the operand order is interval-left, point-right, so an
994
+ * interval-shaped literal beside a scalar element-typed sibling is the LEGAL
995
+ * interval operand of `pointIn(t, span(...))`; under every other operator an
996
+ * interval shape against a scalar sibling stays refused.
1052
997
  */
1053
998
  function taggedCmpLiteral(context, sibling, value, op) {
1054
999
  if (sibling === "measure") {
@@ -1069,16 +1014,16 @@ function taggedCmpLiteral(context, sibling, value, op) {
1069
1014
  return taggedLiteral(context, sibling, value);
1070
1015
  }
1071
1016
  /** Creates one rule-scoped variable numberer. */
1072
- function makeVarIds() {
1017
+ function freshVarIds() {
1073
1018
  const assigned = new Map();
1074
1019
  return {
1075
- of(name) {
1076
- const existing = assigned.get(name);
1020
+ of(ref) {
1021
+ const existing = assigned.get(ref);
1077
1022
  if (existing !== undefined) {
1078
1023
  return existing;
1079
1024
  }
1080
1025
  const id = assigned.size;
1081
- assigned.set(name, id);
1026
+ assigned.set(ref, id);
1082
1027
  return id;
1083
1028
  }
1084
1029
  };
@@ -1093,10 +1038,7 @@ function paramIdOf(ctx, name) {
1093
1038
  }
1094
1039
  /**
1095
1040
  * Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
1096
- * same edb source its ordinal is its record-declaration slot exactly like
1097
- * an ordinary relation's — with field ordinals over the SEALED shape: `id`
1098
- * at 0, each payload column at its declared index + 1 (`sealedFieldsOf`
1099
- * carries the shift; the lowering golden pins it).
1041
+ * same edb source, with field ordinals over the SEALED shape.
1100
1042
  */
1101
1043
  function lowerAtom(ctx, atom, ids) {
1102
1044
  const member = ctx.theory.relations[atom.relation.name];
@@ -1119,17 +1061,12 @@ function lowerAtom(ctx, atom, ids) {
1119
1061
  });
1120
1062
  return { source: { kind: "edb", relation: relationId }, bindings };
1121
1063
  }
1122
- /**
1123
- * Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
1124
- * existing param-set term over its content-addressed registry entry — the
1125
- * program IR is byte-identical to the same set spelled `r.inSet`; the SDK
1126
- * supplies the translated member set itself at execute (`wireParams`).
1127
- */
1064
+ /** Lowers one binding term. A membership ARRAY lowers to the existing param-set term over its content-addressed entry. */
1128
1065
  function lowerBindingTerm(ctx, context, binding, ids) {
1129
1066
  const bound = binding.term;
1130
1067
  switch (bound.kind) {
1131
1068
  case "var":
1132
- return { kind: "var", var: ids.of(bound.name) };
1069
+ return { kind: "var", var: ids.of(bound.ref) };
1133
1070
  case "param":
1134
1071
  return { kind: "param", param: paramIdOf(ctx, bound.name) };
1135
1072
  case "setParam":
@@ -1140,34 +1077,45 @@ function lowerBindingTerm(ctx, context, binding, ids) {
1140
1077
  return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
1141
1078
  }
1142
1079
  }
1143
- /** Lowers one idb atom: positional head bindings, `FieldId(i)` = head position i. */
1144
- function lowerIdbAtom(ctx, rec, vars, ids) {
1080
+ /**
1081
+ * Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
1082
+ * head position i. Every head column of the rec must be bound (a missing key
1083
+ * is refused pointed); the var-id assignment order is head order, so the
1084
+ * first-use numbering matches the name-keyed edition exactly.
1085
+ */
1086
+ function lowerIdbAtom(ctx, rec, bindings, ids) {
1145
1087
  const pred = ctx.recIds.get(rec);
1146
1088
  if (pred === undefined) {
1147
1089
  throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`);
1148
1090
  }
1149
- const arity = rec.rules[0]?.select.length;
1150
- if (arity !== undefined && vars.length !== arity) {
1151
- throw errors.new(`query lowering: idb ${rec.name} takes ${arity} positions, got ${vars.length}`);
1091
+ const head = rec.rules[0];
1092
+ if (head === undefined) {
1093
+ throw errors.new(`query lowering: rec ${rec.name} has no rules`);
1152
1094
  }
1153
- const bindings = vars.map(function lowerPosition(name, position) {
1154
- return [position, { kind: "var", var: ids.of(name) }];
1095
+ const irBindings = head.finds.map(function lowerPosition(column, position) {
1096
+ const binding = bindings.find(function byKey(candidate) {
1097
+ return candidate.key === column.name;
1098
+ });
1099
+ if (binding === undefined) {
1100
+ throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`);
1101
+ }
1102
+ return [position, { kind: "var", var: ids.of(binding.ref) }];
1155
1103
  });
1156
- return { source: { kind: "idb", pred }, bindings };
1104
+ return { source: { kind: "idb", pred }, bindings: irBindings };
1157
1105
  }
1158
1106
  /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
1159
- function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
1107
+ function lowerCmpTerm(ctx, side, sibling, ids, op) {
1160
1108
  switch (side.kind) {
1161
1109
  case "var":
1162
- return { kind: "var", var: ids.of(side.name) };
1110
+ return { kind: "var", var: ids.of(side.ref) };
1163
1111
  case "param":
1164
1112
  return { kind: "param", param: paramIdOf(ctx, side.name) };
1165
1113
  case "setParam":
1166
1114
  return { kind: "paramSet", param: paramIdOf(ctx, side.name) };
1167
1115
  case "measure":
1168
- return { kind: "measure", var: ids.of(side.name) };
1116
+ return { kind: "measure", var: ids.of(side.ref) };
1169
1117
  case "literal": {
1170
- const anchor = cmpAnchorOf(ctx, rule, sibling);
1118
+ const anchor = cmpAnchorOf(ctx, sibling);
1171
1119
  if (anchor === undefined) {
1172
1120
  throw errors.new("query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it");
1173
1121
  }
@@ -1175,10 +1123,10 @@ function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
1175
1123
  }
1176
1124
  }
1177
1125
  }
1178
- /** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
1179
- function cmpAnchorOf(ctx, rule, sibling) {
1126
+ /** Resolves the anchor a comparison literal tags by: the sibling variable's field, the measure, or an anchored param. */
1127
+ function cmpAnchorOf(ctx, sibling) {
1180
1128
  if (sibling.kind === "var") {
1181
- return rule.varFields[sibling.name]?.field;
1129
+ return sibling.ref.field;
1182
1130
  }
1183
1131
  if (sibling.kind === "measure") {
1184
1132
  return "measure";
@@ -1189,7 +1137,7 @@ function cmpAnchorOf(ctx, rule, sibling) {
1189
1137
  return undefined;
1190
1138
  }
1191
1139
  /** Lowers one comparison. */
1192
- function lowerComparison(ctx, rule, cmp, ids) {
1140
+ function lowerComparison(ctx, cmp, ids) {
1193
1141
  if (cmp.op === "allen") {
1194
1142
  const maskData = cmp.mask;
1195
1143
  if (maskData === undefined) {
@@ -1200,29 +1148,29 @@ function lowerComparison(ctx, rule, cmp, ids) {
1200
1148
  : { kind: "param", param: paramIdOf(ctx, maskData.name) };
1201
1149
  return {
1202
1150
  op: { kind: "allen", mask },
1203
- lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, "allen"),
1204
- rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, "allen")
1151
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
1152
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
1205
1153
  };
1206
1154
  }
1207
1155
  return {
1208
1156
  op: { kind: cmp.op },
1209
- lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, cmp.op),
1210
- rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, cmp.op)
1157
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
1158
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op)
1211
1159
  };
1212
1160
  }
1213
1161
  /** Lowers one condition node (comparison leaf or and/or tree). */
1214
- function lowerCondition(ctx, rule, cond, ids) {
1162
+ function lowerCondition(ctx, cond, ids) {
1215
1163
  if (cond.kind === "cmp") {
1216
- return { kind: "leaf", cmp: lowerComparison(ctx, rule, cond, ids) };
1164
+ return { kind: "leaf", cmp: lowerComparison(ctx, cond, ids) };
1217
1165
  }
1218
1166
  return {
1219
1167
  kind: cond.op,
1220
1168
  children: cond.children.map(function lowerChild(child) {
1221
- return lowerCondition(ctx, rule, child, ids);
1169
+ return lowerCondition(ctx, child, ids);
1222
1170
  })
1223
1171
  };
1224
1172
  }
1225
- /** Lowers one select entry to its per-rule find term. */
1173
+ /** Lowers one find entry to its per-rule find term. */
1226
1174
  function lowerFind(entry, ids) {
1227
1175
  if (entry.kind === "var") {
1228
1176
  return { kind: "var", var: ids.of(entry.over) };
@@ -1237,10 +1185,10 @@ function lowerFind(entry, ids) {
1237
1185
  case "countDistinct":
1238
1186
  return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) };
1239
1187
  case "fold": {
1240
- if (typeof agg.over === "string") {
1241
- return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
1188
+ if ("duration" in agg.over) {
1189
+ return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
1242
1190
  }
1243
- return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
1191
+ return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
1244
1192
  }
1245
1193
  case "arg":
1246
1194
  return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) };
@@ -1263,7 +1211,7 @@ function headOpOf(agg) {
1263
1211
  return "pack";
1264
1212
  }
1265
1213
  }
1266
- /** One select entry's var-free head shape. */
1214
+ /** One find entry's var-free head shape. */
1267
1215
  function headTermOf(column) {
1268
1216
  const entry = column.entry;
1269
1217
  if (entry.kind === "var" || entry.kind === "measure") {
@@ -1273,7 +1221,7 @@ function headTermOf(column) {
1273
1221
  }
1274
1222
  /** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
1275
1223
  function lowerRule(ctx, rule) {
1276
- const ids = makeVarIds();
1224
+ const ids = freshVarIds();
1277
1225
  const atoms = [];
1278
1226
  const negated = [];
1279
1227
  const conditions = [];
@@ -1288,17 +1236,17 @@ function lowerRule(ctx, rule) {
1288
1236
  break;
1289
1237
  }
1290
1238
  case "idb": {
1291
- atoms.push(lowerIdbAtom(ctx, item.rec, item.vars, ids));
1239
+ atoms.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids));
1292
1240
  break;
1293
1241
  }
1294
1242
  case "cond": {
1295
- conditions.push(lowerCondition(ctx, rule, item.cond, ids));
1243
+ conditions.push(lowerCondition(ctx, item.cond, ids));
1296
1244
  break;
1297
1245
  }
1298
1246
  }
1299
1247
  }
1300
1248
  return {
1301
- finds: rule.select.map(function findOf(column) {
1249
+ finds: rule.finds.map(function findOf(column) {
1302
1250
  return lowerFind(column.entry, ids);
1303
1251
  }),
1304
1252
  atoms,
@@ -1309,11 +1257,7 @@ function lowerRule(ctx, rule) {
1309
1257
  /**
1310
1258
  * Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
1311
1259
  * recs in declaration order (`PredId` = index), the output predicate
1312
- * (rules + head) appended last. Relations lower by declaration ordinal,
1313
- * the law the engine's own manifest pins; `db.prepare` re-verifies the
1314
- * alignment against the live manifest before sending. Every registered
1315
- * param must carry a field anchor by now — an unanchorable param (its
1316
- * every use beside a literal) is refused here, naming it.
1260
+ * appended last. Every registered param must carry a field anchor by now.
1317
1261
  */
1318
1262
  function lowerQuery(q) {
1319
1263
  const theory = q.schema;
@@ -1341,14 +1285,14 @@ function lowerQuery(q) {
1341
1285
  throw errors.new(`query lowering: rec ${rec.name} has no rules`);
1342
1286
  }
1343
1287
  return {
1344
- head: head.select.map(headTermOf),
1288
+ head: head.finds.map(headTermOf),
1345
1289
  rules: rec.rules.map(function lowerRecRule(rule) {
1346
1290
  return lowerRule(ctx, rule);
1347
1291
  })
1348
1292
  };
1349
1293
  });
1350
1294
  predicates.push({
1351
- head: q.data.select.map(headTermOf),
1295
+ head: q.data.finds.map(headTermOf),
1352
1296
  rules: q.data.rules.map(function lowerOutputRule(rule) {
1353
1297
  return lowerRule(ctx, rule);
1354
1298
  })