@bjornpagen/bumbledb 0.4.0 → 0.6.0

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