@bjornpagen/bumbledb 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/COOKBOOK.md +140 -136
  2. package/README.md +24 -11
  3. package/dist/closed.d.ts +7 -5
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +4 -1
  6. package/dist/closed.js.map +1 -1
  7. package/dist/count.js +1 -1
  8. package/dist/count.js.map +1 -1
  9. package/dist/db.d.ts +110 -73
  10. package/dist/db.d.ts.map +1 -1
  11. package/dist/db.js +172 -116
  12. package/dist/db.js.map +1 -1
  13. package/dist/exhume.d.ts +22 -10
  14. package/dist/exhume.d.ts.map +1 -1
  15. package/dist/exhume.js +42 -9
  16. package/dist/exhume.js.map +1 -1
  17. package/dist/face.d.ts +10 -6
  18. package/dist/face.d.ts.map +1 -1
  19. package/dist/face.js.map +1 -1
  20. package/dist/fields.d.ts +38 -16
  21. package/dist/fields.d.ts.map +1 -1
  22. package/dist/fields.js +33 -8
  23. package/dist/fields.js.map +1 -1
  24. package/dist/index.d.ts +9 -6
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +5 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/lower.d.ts +9 -9
  29. package/dist/lower.d.ts.map +1 -1
  30. package/dist/lower.js +17 -12
  31. package/dist/lower.js.map +1 -1
  32. package/dist/marshal.d.ts +17 -7
  33. package/dist/marshal.d.ts.map +1 -1
  34. package/dist/marshal.js +32 -10
  35. package/dist/marshal.js.map +1 -1
  36. package/dist/native.d.ts +60 -9
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/native.js.map +1 -1
  39. package/dist/query/atom.d.ts +199 -203
  40. package/dist/query/atom.d.ts.map +1 -1
  41. package/dist/query/atom.js +39 -48
  42. package/dist/query/atom.js.map +1 -1
  43. package/dist/query/find.d.ts +116 -0
  44. package/dist/query/find.d.ts.map +1 -0
  45. package/dist/query/{select.js → find.js} +22 -22
  46. package/dist/query/find.js.map +1 -0
  47. package/dist/query/lower.d.ts +131 -158
  48. package/dist/query/lower.d.ts.map +1 -1
  49. package/dist/query/lower.js +505 -494
  50. package/dist/query/lower.js.map +1 -1
  51. package/dist/query/predicate.d.ts +22 -14
  52. package/dist/query/predicate.d.ts.map +1 -1
  53. package/dist/query/predicate.js +35 -42
  54. package/dist/query/predicate.js.map +1 -1
  55. package/dist/query/run.d.ts +8 -8
  56. package/dist/query/run.d.ts.map +1 -1
  57. package/dist/query/run.js +16 -21
  58. package/dist/query/run.js.map +1 -1
  59. package/dist/query/scope.d.ts +139 -74
  60. package/dist/query/scope.d.ts.map +1 -1
  61. package/dist/query/scope.js +80 -33
  62. package/dist/query/scope.js.map +1 -1
  63. package/dist/relation.d.ts.map +1 -1
  64. package/dist/relation.js +3 -1
  65. package/dist/relation.js.map +1 -1
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +12 -2
  68. package/dist/schema.js.map +1 -1
  69. package/dist/spec.d.ts +20 -10
  70. package/dist/spec.d.ts.map +1 -1
  71. package/dist/spec.js.map +1 -1
  72. package/dist/statements.d.ts +22 -2
  73. package/dist/statements.d.ts.map +1 -1
  74. package/dist/statements.js +26 -5
  75. package/dist/statements.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/closed.ts +13 -7
  78. package/src/count.ts +1 -1
  79. package/src/db.ts +293 -177
  80. package/src/exhume.ts +48 -11
  81. package/src/face.ts +13 -6
  82. package/src/fields.ts +51 -15
  83. package/src/index.ts +12 -8
  84. package/src/lower.ts +17 -12
  85. package/src/marshal.ts +38 -13
  86. package/src/native.ts +59 -8
  87. package/src/query/atom.ts +297 -275
  88. package/src/query/find.ts +212 -0
  89. package/src/query/lower.ts +692 -724
  90. package/src/query/predicate.ts +37 -45
  91. package/src/query/run.ts +17 -22
  92. package/src/query/scope.ts +184 -90
  93. package/src/relation.ts +3 -1
  94. package/src/schema.ts +14 -2
  95. package/src/spec.ts +21 -9
  96. package/src/statements.ts +30 -6
  97. package/dist/query/select.d.ts +0 -128
  98. package/dist/query/select.d.ts.map +0 -1
  99. package/dist/query/select.js.map +0 -1
  100. package/src/query/select.ts +0 -215
@@ -1,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,213 +286,217 @@ 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
  }
304
+ if (cond.cond === "notIdb") {
305
+ const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
306
+ return value !== undefined;
307
+ }));
308
+ return notIdbAdvance(context, state, cond.target, bindings);
309
+ }
305
310
  if (cond.cond === "not") {
306
311
  const relation = cond.relation;
307
312
  const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
308
313
  return value !== undefined;
309
314
  }));
310
- const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes);
311
- return {
315
+ const resolved = resolveBindings(context, `negated relation ${relation.name}`, relation, bindings);
316
+ return Object.freeze({
312
317
  items: Object.freeze([...state.items, Object.freeze({ kind: "negated", atom: resolved.atom })]),
313
- varFields: state.varFields,
318
+ bound: state.bound,
314
319
  paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
315
- };
320
+ });
316
321
  }
317
322
  const uses = [];
318
- const data = condDataOf(cond, state.varFields, uses);
319
- return {
323
+ const data = condDataOf(cond, uses);
324
+ return Object.freeze({
320
325
  items: Object.freeze([...state.items, Object.freeze({ kind: "cond", cond: data })]),
321
- varFields: state.varFields,
326
+ bound: state.bound,
322
327
  paramUses: Object.freeze([...state.paramUses, ...uses])
323
- };
324
- }
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()`);
330
- }
331
- return variable.name;
332
328
  });
333
- return {
334
- items: Object.freeze([...state.items, Object.freeze({ kind: "idb", rec, vars: Object.freeze(names) })]),
335
- varFields: state.varFields,
336
- paramUses: state.paramUses
337
- };
338
- }
339
- /** Narrows a select entry to an aggregate value. */
340
- function isAggregateEntry(value) {
341
- return typeof value === "object" && value !== null && "agg" in value;
342
329
  }
343
330
  /**
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.
331
+ * Extends a rule state with one `idb` atom (a named record over head keys;
332
+ * vars validated at completion). A POSITIVE idb atom is a positive
333
+ * occurrence exactly as the engine represents it (`check_atoms` walks Idb
334
+ * and Edb in one loop), so its variables GROUND: they enter the rule's
335
+ * boundness set, may ride the head, and satisfy negation safety — the
336
+ * idb-only identity projection of a finished stratum is spellable with no
337
+ * re-grounding join. A NEGATED one binds nothing, only rejects.
348
338
  */
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
- });
339
+ function advanceIdb(state, rec, bindings, negated) {
340
+ const resolved = [];
341
+ for (const [key, value] of Object.entries(bindings)) {
342
+ if (value === undefined) {
343
+ continue;
344
+ }
345
+ if (!isTerm(value) || value[term] !== "var") {
346
+ throw errors.new(`idb ${rec.name}: position ${key} takes a variable — bind literals and params through where()/match()`);
364
347
  }
365
- throw errors.new(`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`);
348
+ resolved.push(Object.freeze({ key, ref: value }));
366
349
  }
367
- if (isAggregateEntry(entry)) {
368
- return aggregateColumnOf(entry);
350
+ const bound = new Set(state.bound);
351
+ if (!negated) {
352
+ for (const binding of resolved) {
353
+ bound.add(binding.ref);
354
+ }
369
355
  }
370
- throw errors.new("query select: not a select entry — select takes variable names, duration(v), or aggregates");
356
+ return Object.freeze({
357
+ items: Object.freeze([
358
+ ...state.items,
359
+ Object.freeze({ kind: "idb", rec, bindings: Object.freeze(resolved), negated })
360
+ ]),
361
+ bound,
362
+ paramUses: state.paramUses
363
+ });
371
364
  }
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
- });
365
+ /** Narrows a find entry to an aggregate value. */
366
+ function isAggregateEntry(value) {
367
+ return typeof value === "object" && value !== null && "agg" in value;
368
+ }
369
+ /** Narrows a value to a variable term, else a pointed refusal. */
370
+ function asVarTerm(context, value) {
371
+ if (isTerm(value) && value[term] === "var") {
372
+ return value;
380
373
  }
374
+ throw errors.new(`${context}: expected a variable`);
375
+ }
376
+ /** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
377
+ function aggDataOf(name, entry) {
381
378
  const over = entry.over;
382
379
  switch (entry.agg) {
383
380
  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
- }
381
+ return Object.freeze({ op: "count" });
382
+ case "countDistinct":
383
+ return Object.freeze({ op: "countDistinct", over: asVarTerm(`find ${name} (countDistinct)`, over) });
391
384
  case "sum":
392
385
  case "min":
393
386
  case "max": {
394
- if (typeof over === "string") {
395
- return column(over, { op: "fold", fold: entry.agg, over });
387
+ if (isTerm(over) && over[term] === "var") {
388
+ return Object.freeze({ op: "fold", fold: entry.agg, over });
396
389
  }
397
390
  if (isTerm(over) && over[term] === "duration") {
398
- return column(over.name, { op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.name }) });
391
+ return Object.freeze({ op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.over }) });
399
392
  }
400
- throw errors.new(`${entry.agg} takes a variable name or duration(v)`);
393
+ throw errors.new(`find ${name} (${entry.agg}): takes a variable or r.duration(v)`);
401
394
  }
402
395
  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 });
396
+ case "argMin":
397
+ return Object.freeze({
398
+ op: "arg",
399
+ direction: entry.agg,
400
+ over: asVarTerm(`find ${name} (${entry.agg})`, over),
401
+ key: asVarTerm(`find ${name} (${entry.agg} key)`, entry.key)
402
+ });
403
+ case "pack":
404
+ return Object.freeze({ op: "pack", over: asVarTerm(`find ${name} (pack)`, over) });
405
+ default:
406
+ throw errors.new(`find ${name}: unknown aggregate ${entry.agg}`);
407
+ }
408
+ }
409
+ /**
410
+ * Classifies one find entry into its named answer column (the KEY names the
411
+ * column, `count` included). The `slot`/`closed` slices are resolved LATER,
412
+ * at rule completion, where boundness and the mint slots are in hand.
413
+ */
414
+ function findColumnOf(name, entry) {
415
+ if (isTerm(entry)) {
416
+ if (entry[term] === "var") {
417
+ return Object.freeze({
418
+ name,
419
+ entry: Object.freeze({ kind: "var", over: entry }),
420
+ closed: undefined,
421
+ slot: undefined
422
+ });
408
423
  }
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 });
424
+ if (entry[term] === "duration") {
425
+ return Object.freeze({
426
+ name,
427
+ entry: Object.freeze({ kind: "measure", over: entry.over }),
428
+ closed: undefined,
429
+ slot: undefined
430
+ });
414
431
  }
415
- default:
416
- throw errors.new(`unknown aggregate ${entry.agg}`);
432
+ throw errors.new(`find ${name}: a ${entry[term]} is not projectable — find takes variables, r.duration(v), or aggregates`);
433
+ }
434
+ if (isAggregateEntry(entry)) {
435
+ return Object.freeze({
436
+ name,
437
+ entry: Object.freeze({ kind: "aggregate", agg: aggDataOf(name, entry) }),
438
+ closed: undefined,
439
+ slot: undefined
440
+ });
417
441
  }
442
+ throw errors.new(`find ${name}: not a find entry — find takes variables, r.duration(v), or aggregates`);
418
443
  }
419
444
  /**
420
445
  * 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).
446
+ * § orderability): a closed reference is equality-and-membership only.
427
447
  */
428
448
  function closedOrderError(context, position, vocabulary) {
429
449
  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
450
  }
431
- /** The comparison ops under the orderable ban (order roster + point membership — every order-comparison position). */
451
+ /** The comparison ops under the orderable ban (order roster + point membership). */
432
452
  function isOrderOp(op) {
433
453
  return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn";
434
454
  }
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`);
455
+ /** Requires a variable to be bound by a relation atom of the rule (the boundness wall — invisible to the type tier). */
456
+ function assertBound(where, bound, ref) {
457
+ if (!bound.has(ref)) {
458
+ throw errors.new(`${where}: the variable ${ref.label} is not bound by a relation atom of the rule`);
440
459
  }
441
- return slot;
442
460
  }
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`);
461
+ /** Requires a variable to be interval-typed (the measure's and pack's domain), off its own descriptor. */
462
+ function assertInterval(where, ref) {
463
+ if (ref.field.kind !== "interval") {
464
+ throw errors.new(`${where}: ${ref.label} is not interval-typed — the measure is defined over interval-typed variables only`);
465
+ }
466
+ }
467
+ /** Requires a variable's own field to be non-closed (the orderable ban's runtime twin). */
468
+ function assertNotClosed(where, position, ref) {
469
+ const roster = rosterOf(ref.field);
470
+ if (roster !== undefined) {
471
+ throw closedOrderError(where, `${position} ${ref.label}`, roster.name);
448
472
  }
449
473
  }
450
474
  /**
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.
475
+ * The classed mint slot one answer column's VALUES flow from: a projected
476
+ * variable's mint slot, or an Arg-carried payload's. Counts, folds, `pack`
477
+ * and the measure derive numbers/intervals, so they resolve no slot.
458
478
  */
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;
479
+ function findColumnSlotOf(context, column) {
480
+ const entry = column.entry;
481
+ if (entry.kind === "var") {
482
+ return mintSlotOf(context, entry.over);
481
483
  }
482
- for (const child of cond.children) {
483
- validateCond(context, varFields, child);
484
+ if (entry.kind === "aggregate" && entry.agg.op === "arg") {
485
+ return mintSlotOf(context, entry.agg.over);
484
486
  }
487
+ return undefined;
485
488
  }
486
- /** Validates one select column's variable references. */
487
- function validateColumn(context, varFields, column) {
489
+ /** Validates one find column's variable references (boundness + the orderable/interval walls, off the var's own field). */
490
+ function validateColumn(context, bound, column) {
491
+ const where = `${contextLabel(context)} find ${column.name}`;
488
492
  const entry = column.entry;
489
493
  if (entry.kind === "var") {
490
- assertBound(`${context} select ${column.name}`, varFields, entry.over);
494
+ assertBound(where, bound, entry.over);
491
495
  return;
492
496
  }
493
497
  if (entry.kind === "measure") {
494
- assertIntervalBound(`${context} select ${column.name}`, varFields, entry.over);
498
+ assertBound(where, bound, entry.over);
499
+ assertInterval(where, entry.over);
495
500
  return;
496
501
  }
497
502
  const agg = entry.agg;
@@ -499,128 +504,146 @@ function validateColumn(context, varFields, column) {
499
504
  case "count":
500
505
  return;
501
506
  case "countDistinct":
502
- assertBound(`${context} select ${column.name}`, varFields, agg.over);
507
+ assertBound(where, bound, agg.over);
503
508
  return;
504
509
  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
- }
510
+ if ("duration" in agg.over) {
511
+ assertBound(where, bound, agg.over.duration);
512
+ assertInterval(where, agg.over.duration);
511
513
  return;
512
514
  }
513
- assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration);
515
+ assertBound(where, bound, agg.over);
516
+ assertNotClosed(where, `the ${agg.fold} input`, agg.over);
514
517
  return;
515
518
  }
516
519
  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
- }
520
+ assertBound(where, bound, agg.over);
521
+ assertBound(where, bound, agg.key);
522
+ assertNotClosed(where, `the ${agg.direction} key`, agg.key);
523
523
  return;
524
524
  }
525
525
  case "pack":
526
- assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over);
526
+ assertBound(where, bound, agg.over);
527
+ assertInterval(where, agg.over);
527
528
  return;
528
529
  }
529
530
  }
530
531
  /**
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.
532
+ * Validates one condition's variable references against the rule's bound
533
+ * set and, for `eq`/`ne` over two variables, holds the class wall through
534
+ * the mint slots (the unification IS a join; bare pairs only with bare).
539
535
  */
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;
536
+ function validateCond(context, bound, cond) {
537
+ const label = contextLabel(context);
538
+ if (cond.kind === "cmp") {
539
+ for (const side of [cond.lhs, cond.rhs]) {
540
+ if (side.kind === "var") {
541
+ assertBound(label, bound, side.ref);
542
+ const roster = rosterOf(side.ref.field);
543
+ if (isOrderOp(cond.op) && roster !== undefined) {
544
+ throw closedOrderError(label, `the ${cond.op} side ${side.ref.label}`, roster.name);
545
+ }
546
+ }
547
+ if (side.kind === "measure") {
548
+ assertBound(label, bound, side.ref);
549
+ assertInterval(label, side.ref);
550
+ }
551
+ }
552
+ if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
553
+ assertBound(label, bound, cond.lhs.ref);
554
+ assertBound(label, bound, cond.rhs.ref);
555
+ const lhs = mintSlotOf(context, cond.lhs.ref);
556
+ const rhs = mintSlotOf(context, cond.rhs.ref);
557
+ if (!fieldJoins(lhs, rhs)) {
558
+ 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)`);
559
+ }
560
+ }
561
+ return;
550
562
  }
551
- if (over === undefined) {
552
- return undefined;
563
+ for (const child of cond.children) {
564
+ validateCond(context, bound, child);
553
565
  }
554
- return rosterOf(varFields[over]?.field);
555
566
  }
556
567
  /**
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).
568
+ * Validates one `idb` item: every head column of the rec is bound exactly
569
+ * once (a missing or extra key is a pointed error) and each variable joins
570
+ * its head column's classed slot. A POSITIVE idb atom GROUNDS its
571
+ * variables (a positive occurrence, exactly the engine's representation),
572
+ * so no boundness precondition exists; a NEGATED one binds nothing its
573
+ * variables must be positively bound elsewhere in the rule, the same
574
+ * safety rule as EDB negation. When the rec's own rule 0 is in flight
575
+ * (`rec.rules[0]` absent), the completing rule's OWN find columns ARE the
576
+ * head.
562
577
  */
563
- function completeRule(context, state, columns) {
564
- if (columns.length === 0) {
565
- throw errors.new(`${context}: a select needs at least one entry`);
578
+ function validateIdb(context, bound, item, columns) {
579
+ const label = contextLabel(context);
580
+ const head = item.rec.rules[0];
581
+ const headColumns = head !== undefined ? head.finds : columns;
582
+ const headNames = headColumns.map(function nameOf(column) {
583
+ return column.name;
584
+ });
585
+ const keys = item.bindings.map(function keyOf(binding) {
586
+ return binding.key;
587
+ });
588
+ for (const key of keys) {
589
+ if (!headNames.includes(key)) {
590
+ throw errors.new(`${label}: idb ${item.rec.name} binds ${key}, not a head column of ${item.rec.name} (head columns: ${headNames.join(", ")})`);
591
+ }
566
592
  }
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`);
593
+ for (const name of headNames) {
594
+ if (!keys.includes(name)) {
595
+ 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
596
  }
573
- seen.add(column.name);
574
- validateColumn(context, state.varFields, column);
575
597
  }
598
+ for (const binding of item.bindings) {
599
+ if (item.negated && !bound.has(binding.ref)) {
600
+ throw errors.new(`${label}: negated idb ${item.rec.name} names the variable ${binding.ref.label}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`);
601
+ }
602
+ const headColumn = headColumns.find(function byName(column) {
603
+ return column.name === binding.key;
604
+ });
605
+ if (headColumn === undefined || headColumn.slot === undefined) {
606
+ continue;
607
+ }
608
+ const mint = mintSlotOf(context, binding.ref);
609
+ if (!fieldJoins(headColumn.slot, mint)) {
610
+ 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`);
611
+ }
612
+ }
613
+ }
614
+ /**
615
+ * Completes one rule: enriches the find columns (declaration-order-safe
616
+ * keys, boundness validated, each column's classed slot and closed slice
617
+ * resolved), then walks the body walls — negated-atom boundness safety, idb
618
+ * head pairing, and condition validation.
619
+ */
620
+ function completeRule(context, state, rawColumns) {
621
+ const label = contextLabel(context);
622
+ if (rawColumns.length === 0) {
623
+ throw errors.new(`${label}: a find needs at least one entry`);
624
+ }
625
+ const columns = rawColumns.map(function enrichColumn(column) {
626
+ assertDeclarationOrderKey(`${label} find column`, column.name);
627
+ validateColumn(context, state.bound, column);
628
+ const slot = findColumnSlotOf(context, column);
629
+ return Object.freeze({ name: column.name, entry: column.entry, slot, closed: rosterOf(slot?.field) });
630
+ });
576
631
  for (const item of state.items) {
577
632
  if (item.kind === "negated") {
578
633
  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
- }
634
+ if (binding.term.kind === "var" && !state.bound.has(binding.term.ref)) {
635
+ 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
636
  }
589
637
  }
590
638
  }
591
639
  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
- });
640
+ validateIdb(context, state.bound, item, columns);
607
641
  }
608
642
  if (item.kind === "cond") {
609
- validateCond(context, state.varFields, item.cond);
643
+ validateCond(context, state.bound, item.cond);
610
644
  }
611
645
  }
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
- });
646
+ return Object.freeze({ items: state.items, finds: Object.freeze(columns), paramUses: state.paramUses });
624
647
  }
625
648
  /** Builds one typed rule value over completed rule data. */
626
649
  function makeRuleValue(rule) {
@@ -638,7 +661,7 @@ function contextLabel(context) {
638
661
  }
639
662
  }
640
663
  /** Validates and records one `idb` atom per the context's cut. */
641
- function idbAdvance(context, state, target, vars) {
664
+ function idbAdvance(context, state, target, bindings) {
642
665
  if (context.kind === "query") {
643
666
  throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
644
667
  }
@@ -646,36 +669,60 @@ function idbAdvance(context, state, target, vars) {
646
669
  if (target.data !== context.self) {
647
670
  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
671
  }
649
- return advanceIdb(state, context.self, vars);
672
+ return advanceIdb(state, context.self, bindings, false);
650
673
  }
651
674
  if (!context.program.recs.includes(target.data)) {
652
675
  throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
653
676
  }
654
- return advanceIdb(state, target.data, vars);
677
+ return advanceIdb(state, target.data, bindings, false);
655
678
  }
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)`);
679
+ /**
680
+ * Validates and records one NEGATED finished-stratum atom — output rules
681
+ * only: there every rec is a finished set before the output's operator
682
+ * runs (negation OF lower strata is engine-legal; the strata judge refuses
683
+ * only negation *through* a cycle, which the rec-context refusal here
684
+ * makes unwritable).
685
+ */
686
+ function notIdbAdvance(context, state, target, bindings) {
687
+ if (context.kind === "query") {
688
+ throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
689
+ }
690
+ if (context.kind === "rec") {
691
+ throw errors.new(`rec ${context.self.name}: a recursive rule negates no stratum — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished stratum's fold belongs in the output rules`);
692
+ }
693
+ if (!context.program.recs.includes(target.data)) {
694
+ throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
695
+ }
696
+ return advanceIdb(state, target.data, bindings, true);
697
+ }
698
+ /** Classifies one find record per the context (a recursive head projects bound variables only). */
699
+ function findColumns(context, entries) {
700
+ const columns = [];
701
+ for (const [name, entry] of Object.entries(entries)) {
702
+ if (entry === undefined) {
703
+ continue;
661
704
  }
662
- return selectColumnOf(entry);
663
- });
705
+ if (context.kind === "rec" && !(isTerm(entry) && entry[term] === "var")) {
706
+ 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)`);
707
+ }
708
+ columns.push(findColumnOf(name, entry));
709
+ }
710
+ return columns;
664
711
  }
665
712
  /** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
666
713
  function makeRawChain(context, state) {
667
714
  const chain = {
668
715
  match(relation, bindings) {
669
- return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes));
716
+ return makeRawChain(context, advanceMatch(context, state, relation, bindings));
670
717
  },
671
718
  where(cond) {
672
- return makeRawChain(context, advanceWhere(state, cond, context.classes));
719
+ return makeRawChain(context, advanceWhere(context, state, cond));
673
720
  },
674
- idb(target, ...vars) {
675
- return makeRawChain(context, idbAdvance(context, state, target, vars));
721
+ idb(target, bindings) {
722
+ return makeRawChain(context, idbAdvance(context, state, target, bindings));
676
723
  },
677
- select(...entries) {
678
- return makeRuleValue(completeRule(contextLabel(context), state, selectColumns(context, entries)));
724
+ find(entries) {
725
+ return makeRuleValue(completeRule(context, state, findColumns(context, entries)));
679
726
  }
680
727
  };
681
728
  Object.freeze(chain);
@@ -686,7 +733,10 @@ function makeRawScope(context) {
686
733
  const scope = {
687
734
  ...termOps,
688
735
  match(relation, bindings) {
689
- return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes));
736
+ return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings));
737
+ },
738
+ idb(target, bindings) {
739
+ return makeRawChain(context, idbAdvance(context, EMPTY_RULE, target, bindings));
690
740
  }
691
741
  };
692
742
  Object.freeze(scope);
@@ -694,21 +744,19 @@ function makeRawScope(context) {
694
744
  }
695
745
  /**
696
746
  * 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.
747
+ * trusted-admission-seam pattern the other mint guards cite: the raw builder
748
+ * is one runtime shape for every context, and this guard verifies the
749
+ * checkable fact the builder verbs exist before the value is admitted at
750
+ * its TYPED face. The type-level judgments (class-equal joins, the recursion
751
+ * cut) live in the interfaces themselves; boundness is a construction-time
752
+ * validation in this module (object identity is invisible to the type tier).
705
753
  */
706
754
  function isTypedScope(scope) {
707
755
  return typeof scope.match === "function";
708
756
  }
709
757
  /** Builds one query-rule builder (the typed face of the raw builder). */
710
- function makeQueryRuleScope(classes) {
711
- const raw = makeRawScope({ kind: "query", classes });
758
+ function makeQueryRuleScope(theory) {
759
+ const raw = makeRawScope({ kind: "query", classes: theory.classes, theory });
712
760
  if (!isTypedScope(raw)) {
713
761
  throw errors.new("query rule builder construction incomplete");
714
762
  }
@@ -716,7 +764,7 @@ function makeQueryRuleScope(classes) {
716
764
  }
717
765
  /** Builds one output-rule builder over a program's recs. */
718
766
  function makeOutputRuleScope(program) {
719
- const raw = makeRawScope({ kind: "output", program, classes: program.classes });
767
+ const raw = makeRawScope({ kind: "output", program, classes: program.classes, theory: program.theory });
720
768
  if (!isTypedScope(raw)) {
721
769
  throw errors.new("program output rule builder construction incomplete");
722
770
  }
@@ -741,25 +789,7 @@ function headSignature(column) {
741
789
  }
742
790
  return `${column.name}:${agg.op}`;
743
791
  }
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). */
792
+ /** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
763
793
  function anchorRosterOf(anchor) {
764
794
  return anchor === "measure" ? undefined : rosterOf(anchor);
765
795
  }
@@ -769,20 +799,9 @@ function renderParamAnchor(roster) {
769
799
  }
770
800
  /**
771
801
  * 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).
802
+ * rules last — exactly the lowering walk) into the query's registry: first
803
+ * use mints the dense `ParamId`, the first FIELD-ANCHORED use types the
804
+ * wire, and one name keeps one shape AND one closedness.
786
805
  */
787
806
  function paramRegistryOf(recs, rules) {
788
807
  const order = [];
@@ -840,64 +859,79 @@ function paramRegistryOf(recs, rules) {
840
859
  if (entry.orderOp !== undefined && anchorRoster !== undefined) {
841
860
  throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name);
842
861
  }
843
- return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members });
862
+ /**
863
+ * A membership array's handle names are program constants, so the
864
+ * entry stores the resolved IMAGE: each name rides the one
865
+ * roster-verification point (`taggedHandleId`, through
866
+ * `taggedCmpLiteral`) exactly once, HERE — an out-of-roster name
867
+ * fails at build, and every execute returns this frozen value by
868
+ * reference.
869
+ */
870
+ let membership;
871
+ if (entry.members !== undefined) {
872
+ const anchor = entry.anchor;
873
+ if (anchor === undefined) {
874
+ throw errors.new(`query param ${name} lost its membership anchor`);
875
+ }
876
+ membership = Object.freeze({
877
+ kind: "set",
878
+ values: Object.freeze(entry.members.map(function tagMember(member, index) {
879
+ return Object.freeze(taggedCmpLiteral(`membership array ${name}[${index}]`, anchor, member, entry.op));
880
+ }))
881
+ });
882
+ }
883
+ return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, membership });
844
884
  }));
845
885
  }
846
886
  /**
847
887
  * Assembles the runtime query value over completed rules: every rule must
848
888
  * 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.
889
+ * the decode labels and the engine's alignment rule agree), and the param
890
+ * registry folds in program-walk order.
851
891
  */
852
892
  function makeRawQuery(theory, recs, rules) {
853
893
  const first = rules[0];
854
894
  if (first === undefined) {
855
895
  throw errors.new("a query needs at least one rule");
856
896
  }
857
- const signature = first.select.map(headSignature).join(", ");
897
+ const signature = first.finds.map(headSignature).join(", ");
858
898
  rules.forEach(function verifyHead(rule, index) {
859
- const candidate = rule.select.map(headSignature).join(", ");
899
+ const candidate = rule.finds.map(headSignature).join(", ");
860
900
  if (candidate !== signature) {
861
- throw errors.new(`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`);
901
+ throw errors.new(`every rule of a query derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`);
862
902
  }
863
903
  // 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];
904
+ // through one roster, so a union whose rules bind a column at different
905
+ // vocabularies (or one closed, one bare) is refused pointed.
906
+ rule.finds.forEach(function verifyClosedSlice(column, position) {
907
+ const lead = first.finds[position];
870
908
  if (lead !== undefined && column.closed !== lead.closed) {
871
909
  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
910
  }
873
911
  // 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.
912
+ // value space, so the classed mint slot each rule binds the column
913
+ // at must join across rules — the SAME fieldJoins judgment every
914
+ // join/eq/negated-atom position enforces (the SDK holds it because
915
+ // the wire IR carries no domains).
880
916
  if (lead === undefined) {
881
917
  return;
882
918
  }
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)`);
919
+ if (lead.slot !== undefined && column.slot !== undefined && !fieldJoins(lead.slot, column.slot)) {
920
+ 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
921
  }
888
922
  });
889
923
  });
890
924
  const data = Object.freeze({
891
925
  recs: Object.freeze([...recs]),
892
926
  rules: Object.freeze([...rules]),
893
- select: first.select,
927
+ finds: first.finds,
894
928
  params: paramRegistryOf(recs, rules)
895
929
  });
896
930
  const value = {
897
931
  schema: theory,
898
932
  data,
899
933
  rule(build) {
900
- const built = build(makeRawScope({ kind: "query", classes: theory.classes }));
934
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory }));
901
935
  return makeRawQuery(theory, recs, [...rules, built.rule]);
902
936
  }
903
937
  };
@@ -921,16 +955,15 @@ function makeQuery(theory, recs, rules) {
921
955
  return raw;
922
956
  }
923
957
  /**
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.
958
+ * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule` adds
959
+ * one conjunctive rule; multiple rules are the set union. The schema's
960
+ * law-computed class map and theory value ride into every rule builder — the
961
+ * join walls compare against the mint slots off it.
929
962
  */
930
963
  function query(theory) {
931
964
  const start = {
932
965
  rule(build) {
933
- const built = build(makeQueryRuleScope(theory.classes));
966
+ const built = build(makeQueryRuleScope(theory));
934
967
  return makeQuery(theory, [], [built.rule]);
935
968
  }
936
969
  };
@@ -939,13 +972,8 @@ function query(theory) {
939
972
  }
940
973
  /**
941
974
  * 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).
975
+ * roster and translated to its declaration-order row id, tagged u64. THE
976
+ * single roster-verification point of the query surface.
949
977
  */
950
978
  function taggedHandleId(context, closed, value) {
951
979
  if (typeof value !== "string") {
@@ -979,11 +1007,7 @@ function taggedAtElementDomain(context, element, value) {
979
1007
  }
980
1008
  /**
981
1009
  * 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.
1010
+ * structural kind directs the tag, never a guess.
987
1011
  */
988
1012
  function taggedLiteral(context, field, value) {
989
1013
  const roster = rosterOf(field);
@@ -1013,15 +1037,6 @@ function taggedLiteral(context, field, value) {
1013
1037
  if (typeof value !== "string") {
1014
1038
  throw literalShapeError(context, "string", value);
1015
1039
  }
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
1040
  if (!value.isWellFormed()) {
1026
1041
  throw literalShapeError(context, "well-formed string", value);
1027
1042
  }
@@ -1038,17 +1053,13 @@ function taggedLiteral(context, field, value) {
1038
1053
  }
1039
1054
  }
1040
1055
  /**
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).
1056
+ * Tags one host literal at a COMPARISON or PARAM position, where the SIBLING
1057
+ * anchors the type: a measure sibling is u64, an interval-field sibling
1058
+ * contributes its element domain, a scalar sibling its own type. At
1059
+ * `pointIn` the operand order is interval-left, point-right, so an
1060
+ * interval-shaped literal beside a scalar element-typed sibling is the LEGAL
1061
+ * interval operand of `pointIn(t, span(...))`; under every other operator an
1062
+ * interval shape against a scalar sibling stays refused.
1052
1063
  */
1053
1064
  function taggedCmpLiteral(context, sibling, value, op) {
1054
1065
  if (sibling === "measure") {
@@ -1069,16 +1080,16 @@ function taggedCmpLiteral(context, sibling, value, op) {
1069
1080
  return taggedLiteral(context, sibling, value);
1070
1081
  }
1071
1082
  /** Creates one rule-scoped variable numberer. */
1072
- function makeVarIds() {
1083
+ function freshVarIds() {
1073
1084
  const assigned = new Map();
1074
1085
  return {
1075
- of(name) {
1076
- const existing = assigned.get(name);
1086
+ of(ref) {
1087
+ const existing = assigned.get(ref);
1077
1088
  if (existing !== undefined) {
1078
1089
  return existing;
1079
1090
  }
1080
1091
  const id = assigned.size;
1081
- assigned.set(name, id);
1092
+ assigned.set(ref, id);
1082
1093
  return id;
1083
1094
  }
1084
1095
  };
@@ -1093,10 +1104,7 @@ function paramIdOf(ctx, name) {
1093
1104
  }
1094
1105
  /**
1095
1106
  * 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).
1107
+ * same edb source, with field ordinals over the SEALED shape.
1100
1108
  */
1101
1109
  function lowerAtom(ctx, atom, ids) {
1102
1110
  const member = ctx.theory.relations[atom.relation.name];
@@ -1119,17 +1127,12 @@ function lowerAtom(ctx, atom, ids) {
1119
1127
  });
1120
1128
  return { source: { kind: "edb", relation: relationId }, bindings };
1121
1129
  }
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
- */
1130
+ /** Lowers one binding term. A membership ARRAY lowers to the existing param-set term over its content-addressed entry. */
1128
1131
  function lowerBindingTerm(ctx, context, binding, ids) {
1129
1132
  const bound = binding.term;
1130
1133
  switch (bound.kind) {
1131
1134
  case "var":
1132
- return { kind: "var", var: ids.of(bound.name) };
1135
+ return { kind: "var", var: ids.of(bound.ref) };
1133
1136
  case "param":
1134
1137
  return { kind: "param", param: paramIdOf(ctx, bound.name) };
1135
1138
  case "setParam":
@@ -1140,34 +1143,45 @@ function lowerBindingTerm(ctx, context, binding, ids) {
1140
1143
  return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
1141
1144
  }
1142
1145
  }
1143
- /** Lowers one idb atom: positional head bindings, `FieldId(i)` = head position i. */
1144
- function lowerIdbAtom(ctx, rec, vars, ids) {
1146
+ /**
1147
+ * Lowers one idb atom: named bindings placed by HEAD order, `FieldId(i)` =
1148
+ * head position i. Every head column of the rec must be bound (a missing key
1149
+ * is refused pointed); the var-id assignment order is head order, so the
1150
+ * first-use numbering matches the name-keyed edition exactly.
1151
+ */
1152
+ function lowerIdbAtom(ctx, rec, bindings, ids) {
1145
1153
  const pred = ctx.recIds.get(rec);
1146
1154
  if (pred === undefined) {
1147
1155
  throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`);
1148
1156
  }
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}`);
1157
+ const head = rec.rules[0];
1158
+ if (head === undefined) {
1159
+ throw errors.new(`query lowering: rec ${rec.name} has no rules`);
1152
1160
  }
1153
- const bindings = vars.map(function lowerPosition(name, position) {
1154
- return [position, { kind: "var", var: ids.of(name) }];
1161
+ const irBindings = head.finds.map(function lowerPosition(column, position) {
1162
+ const binding = bindings.find(function byKey(candidate) {
1163
+ return candidate.key === column.name;
1164
+ });
1165
+ if (binding === undefined) {
1166
+ throw errors.new(`query lowering: idb ${rec.name} omits head column ${column.name}`);
1167
+ }
1168
+ return [position, { kind: "var", var: ids.of(binding.ref) }];
1155
1169
  });
1156
- return { source: { kind: "idb", pred }, bindings };
1170
+ return { source: { kind: "idb", pred }, bindings: irBindings };
1157
1171
  }
1158
1172
  /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
1159
- function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
1173
+ function lowerCmpTerm(ctx, side, sibling, ids, op) {
1160
1174
  switch (side.kind) {
1161
1175
  case "var":
1162
- return { kind: "var", var: ids.of(side.name) };
1176
+ return { kind: "var", var: ids.of(side.ref) };
1163
1177
  case "param":
1164
1178
  return { kind: "param", param: paramIdOf(ctx, side.name) };
1165
1179
  case "setParam":
1166
1180
  return { kind: "paramSet", param: paramIdOf(ctx, side.name) };
1167
1181
  case "measure":
1168
- return { kind: "measure", var: ids.of(side.name) };
1182
+ return { kind: "measure", var: ids.of(side.ref) };
1169
1183
  case "literal": {
1170
- const anchor = cmpAnchorOf(ctx, rule, sibling);
1184
+ const anchor = cmpAnchorOf(ctx, sibling);
1171
1185
  if (anchor === undefined) {
1172
1186
  throw errors.new("query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it");
1173
1187
  }
@@ -1175,10 +1189,10 @@ function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
1175
1189
  }
1176
1190
  }
1177
1191
  }
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) {
1192
+ /** Resolves the anchor a comparison literal tags by: the sibling variable's field, the measure, or an anchored param. */
1193
+ function cmpAnchorOf(ctx, sibling) {
1180
1194
  if (sibling.kind === "var") {
1181
- return rule.varFields[sibling.name]?.field;
1195
+ return sibling.ref.field;
1182
1196
  }
1183
1197
  if (sibling.kind === "measure") {
1184
1198
  return "measure";
@@ -1189,7 +1203,7 @@ function cmpAnchorOf(ctx, rule, sibling) {
1189
1203
  return undefined;
1190
1204
  }
1191
1205
  /** Lowers one comparison. */
1192
- function lowerComparison(ctx, rule, cmp, ids) {
1206
+ function lowerComparison(ctx, cmp, ids) {
1193
1207
  if (cmp.op === "allen") {
1194
1208
  const maskData = cmp.mask;
1195
1209
  if (maskData === undefined) {
@@ -1200,29 +1214,29 @@ function lowerComparison(ctx, rule, cmp, ids) {
1200
1214
  : { kind: "param", param: paramIdOf(ctx, maskData.name) };
1201
1215
  return {
1202
1216
  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")
1217
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
1218
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
1205
1219
  };
1206
1220
  }
1207
1221
  return {
1208
1222
  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)
1223
+ lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, cmp.op),
1224
+ rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, cmp.op)
1211
1225
  };
1212
1226
  }
1213
1227
  /** Lowers one condition node (comparison leaf or and/or tree). */
1214
- function lowerCondition(ctx, rule, cond, ids) {
1228
+ function lowerCondition(ctx, cond, ids) {
1215
1229
  if (cond.kind === "cmp") {
1216
- return { kind: "leaf", cmp: lowerComparison(ctx, rule, cond, ids) };
1230
+ return { kind: "leaf", cmp: lowerComparison(ctx, cond, ids) };
1217
1231
  }
1218
1232
  return {
1219
1233
  kind: cond.op,
1220
1234
  children: cond.children.map(function lowerChild(child) {
1221
- return lowerCondition(ctx, rule, child, ids);
1235
+ return lowerCondition(ctx, child, ids);
1222
1236
  })
1223
1237
  };
1224
1238
  }
1225
- /** Lowers one select entry to its per-rule find term. */
1239
+ /** Lowers one find entry to its per-rule find term. */
1226
1240
  function lowerFind(entry, ids) {
1227
1241
  if (entry.kind === "var") {
1228
1242
  return { kind: "var", var: ids.of(entry.over) };
@@ -1237,10 +1251,10 @@ function lowerFind(entry, ids) {
1237
1251
  case "countDistinct":
1238
1252
  return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) };
1239
1253
  case "fold": {
1240
- if (typeof agg.over === "string") {
1241
- return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
1254
+ if ("duration" in agg.over) {
1255
+ return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
1242
1256
  }
1243
- return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
1257
+ return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
1244
1258
  }
1245
1259
  case "arg":
1246
1260
  return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) };
@@ -1263,7 +1277,7 @@ function headOpOf(agg) {
1263
1277
  return "pack";
1264
1278
  }
1265
1279
  }
1266
- /** One select entry's var-free head shape. */
1280
+ /** One find entry's var-free head shape. */
1267
1281
  function headTermOf(column) {
1268
1282
  const entry = column.entry;
1269
1283
  if (entry.kind === "var" || entry.kind === "measure") {
@@ -1273,7 +1287,7 @@ function headTermOf(column) {
1273
1287
  }
1274
1288
  /** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
1275
1289
  function lowerRule(ctx, rule) {
1276
- const ids = makeVarIds();
1290
+ const ids = freshVarIds();
1277
1291
  const atoms = [];
1278
1292
  const negated = [];
1279
1293
  const conditions = [];
@@ -1288,17 +1302,18 @@ function lowerRule(ctx, rule) {
1288
1302
  break;
1289
1303
  }
1290
1304
  case "idb": {
1291
- atoms.push(lowerIdbAtom(ctx, item.rec, item.vars, ids));
1305
+ const bucket = item.negated ? negated : atoms;
1306
+ bucket.push(lowerIdbAtom(ctx, item.rec, item.bindings, ids));
1292
1307
  break;
1293
1308
  }
1294
1309
  case "cond": {
1295
- conditions.push(lowerCondition(ctx, rule, item.cond, ids));
1310
+ conditions.push(lowerCondition(ctx, item.cond, ids));
1296
1311
  break;
1297
1312
  }
1298
1313
  }
1299
1314
  }
1300
1315
  return {
1301
- finds: rule.select.map(function findOf(column) {
1316
+ finds: rule.finds.map(function findOf(column) {
1302
1317
  return lowerFind(column.entry, ids);
1303
1318
  }),
1304
1319
  atoms,
@@ -1309,11 +1324,7 @@ function lowerRule(ctx, rule) {
1309
1324
  /**
1310
1325
  * Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
1311
1326
  * 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.
1327
+ * appended last. Every registered param must carry a field anchor by now.
1317
1328
  */
1318
1329
  function lowerQuery(q) {
1319
1330
  const theory = q.schema;
@@ -1341,14 +1352,14 @@ function lowerQuery(q) {
1341
1352
  throw errors.new(`query lowering: rec ${rec.name} has no rules`);
1342
1353
  }
1343
1354
  return {
1344
- head: head.select.map(headTermOf),
1355
+ head: head.finds.map(headTermOf),
1345
1356
  rules: rec.rules.map(function lowerRecRule(rule) {
1346
1357
  return lowerRule(ctx, rule);
1347
1358
  })
1348
1359
  };
1349
1360
  });
1350
1361
  predicates.push({
1351
- head: q.data.select.map(headTermOf),
1362
+ head: q.data.finds.map(headTermOf),
1352
1363
  rules: q.data.rules.map(function lowerOutputRule(rule) {
1353
1364
  return lowerRule(ctx, rule);
1354
1365
  })