@bjornpagen/bumbledb 0.1.0 → 0.3.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 (112) hide show
  1. package/COOKBOOK.md +1450 -0
  2. package/README.md +69 -30
  3. package/dist/closed.d.ts +157 -38
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +232 -47
  6. package/dist/closed.js.map +1 -1
  7. package/dist/count.d.ts +64 -17
  8. package/dist/count.d.ts.map +1 -1
  9. package/dist/count.js +37 -14
  10. package/dist/count.js.map +1 -1
  11. package/dist/db.d.ts +27 -8
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +33 -10
  14. package/dist/db.js.map +1 -1
  15. package/dist/exhume.d.ts +12 -11
  16. package/dist/exhume.d.ts.map +1 -1
  17. package/dist/exhume.js +7 -6
  18. package/dist/exhume.js.map +1 -1
  19. package/dist/face.d.ts +135 -30
  20. package/dist/face.d.ts.map +1 -1
  21. package/dist/face.js +44 -19
  22. package/dist/face.js.map +1 -1
  23. package/dist/fields.d.ts +130 -130
  24. package/dist/fields.d.ts.map +1 -1
  25. package/dist/fields.js +60 -94
  26. package/dist/fields.js.map +1 -1
  27. package/dist/index.d.ts +29 -24
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +19 -14
  30. package/dist/index.js.map +1 -1
  31. package/dist/law.d.ts +224 -0
  32. package/dist/law.d.ts.map +1 -0
  33. package/dist/law.js +224 -0
  34. package/dist/law.js.map +1 -0
  35. package/dist/lower.d.ts +17 -7
  36. package/dist/lower.d.ts.map +1 -1
  37. package/dist/lower.js +66 -24
  38. package/dist/lower.js.map +1 -1
  39. package/dist/marshal.d.ts +36 -21
  40. package/dist/marshal.d.ts.map +1 -1
  41. package/dist/marshal.js +41 -25
  42. package/dist/marshal.js.map +1 -1
  43. package/dist/native.d.ts +6 -2
  44. package/dist/native.d.ts.map +1 -1
  45. package/dist/native.js +2 -2
  46. package/dist/native.js.map +1 -1
  47. package/dist/query/atom.d.ts +370 -174
  48. package/dist/query/atom.d.ts.map +1 -1
  49. package/dist/query/atom.js +75 -172
  50. package/dist/query/atom.js.map +1 -1
  51. package/dist/query/lower.d.ts +312 -109
  52. package/dist/query/lower.d.ts.map +1 -1
  53. package/dist/query/lower.js +906 -387
  54. package/dist/query/lower.js.map +1 -1
  55. package/dist/query/predicate.d.ts +70 -88
  56. package/dist/query/predicate.d.ts.map +1 -1
  57. package/dist/query/predicate.js +118 -72
  58. package/dist/query/predicate.js.map +1 -1
  59. package/dist/query/run.d.ts +19 -16
  60. package/dist/query/run.d.ts.map +1 -1
  61. package/dist/query/run.js +31 -24
  62. package/dist/query/run.js.map +1 -1
  63. package/dist/query/scope.d.ts +178 -127
  64. package/dist/query/scope.d.ts.map +1 -1
  65. package/dist/query/scope.js +111 -108
  66. package/dist/query/scope.js.map +1 -1
  67. package/dist/query/select.d.ts +102 -80
  68. package/dist/query/select.d.ts.map +1 -1
  69. package/dist/query/select.js +39 -34
  70. package/dist/query/select.js.map +1 -1
  71. package/dist/relation.d.ts +48 -38
  72. package/dist/relation.d.ts.map +1 -1
  73. package/dist/relation.js +24 -19
  74. package/dist/relation.js.map +1 -1
  75. package/dist/schema.d.ts +41 -3
  76. package/dist/schema.d.ts.map +1 -1
  77. package/dist/schema.js +34 -9
  78. package/dist/schema.js.map +1 -1
  79. package/dist/spec.d.ts +7 -5
  80. package/dist/spec.d.ts.map +1 -1
  81. package/dist/spec.js.map +1 -1
  82. package/dist/statements.d.ts +91 -36
  83. package/dist/statements.d.ts.map +1 -1
  84. package/dist/statements.js +42 -12
  85. package/dist/statements.js.map +1 -1
  86. package/package.json +4 -3
  87. package/src/closed.ts +448 -95
  88. package/src/count.ts +112 -18
  89. package/src/db.ts +45 -17
  90. package/src/exhume.ts +12 -11
  91. package/src/face.ts +211 -41
  92. package/src/fields.ts +199 -234
  93. package/src/index.ts +78 -61
  94. package/src/law.ts +519 -0
  95. package/src/lower.ts +82 -27
  96. package/src/marshal.ts +48 -30
  97. package/src/native.ts +8 -4
  98. package/src/query/atom.ts +525 -374
  99. package/src/query/lower.ts +1469 -538
  100. package/src/query/predicate.ts +208 -161
  101. package/src/query/run.ts +35 -25
  102. package/src/query/scope.ts +264 -218
  103. package/src/query/select.ts +168 -93
  104. package/src/relation.ts +47 -47
  105. package/src/schema.ts +69 -18
  106. package/src/spec.ts +7 -5
  107. package/src/statements.ts +124 -41
  108. package/dist/brand.d.ts +0 -59
  109. package/dist/brand.d.ts.map +0 -1
  110. package/dist/brand.js +0 -47
  111. package/dist/brand.js.map +0 -1
  112. package/src/brand.ts +0 -82
@@ -1,156 +1,717 @@
1
1
  /**
2
- * `query()` and the IR lowering (PRD-08). A query is built inside a scope
3
- * callback (variable identity is lexical and typed) and is an INERT value:
4
- * `Query<Rels, Row, Params>` with `Row` inferred from `select` and
5
- * `Params` from every parameter the returned rules (and the predicates
6
- * they reach) use. Lowering is a pure function of the query value down to
7
- * the bridge's `ProgramIr` (`bumbledb/crates/bumbledb/src/ir.rs`, the
8
- * bijection target): relations and predicates by declaration ordinal (the
9
- * declaration-order-is-ids law the engine's manifest pins), variables by
10
- * dense per-rule first-occurrence ids (rule-scoped, exactly as the IR
11
- * scopes them), params by scope declaration order. Lowering is STABLE
12
- * the same query value lowers to deeply-equal IR every time, and two
13
- * identically-written queries lower identically (prepared-query caching
14
- * upstream keys on this). Construction validates negation safety (typed,
15
- * naming the variable earlier and warmer than the engine's refusal);
16
- * everything else (strata, types, aggregates rosters, rule caps) is the
17
- * ENGINE's judge, surfacing its typed errors at prepare. No invented
18
- * limits: rule counts and predicate counts are never pre-checked here.
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.
19
26
  */
20
27
  import * as errors from "@superbuilders/errors";
21
- import { phantom } from "#brand.ts";
22
28
  import { assertDeclarationOrderKey } from "#fields.ts";
23
- import { makePredicate } from "#query/predicate.ts";
24
- import { createRegistry, isTerm, scopeAllenParam, scopeParam, scopeParamSet, scopeVar, term } from "#query/scope.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";
32
+ /** The frozen constructor vocabulary every rule builder spreads. */
33
+ const termOps = Object.freeze({
34
+ var: makeVar,
35
+ vars: makeVars,
36
+ param: makeParam,
37
+ inSet: makeSetParam,
38
+ maskParam: makeMaskParam,
39
+ duration: makeDuration,
40
+ eq,
41
+ ne,
42
+ lt,
43
+ le,
44
+ gt,
45
+ ge,
46
+ pointIn,
47
+ covers,
48
+ allen,
49
+ and,
50
+ or,
51
+ not,
52
+ count,
53
+ countDistinct,
54
+ sum,
55
+ min,
56
+ max,
57
+ argMax,
58
+ argMin,
59
+ pack
60
+ });
61
+ /** The empty rule state. */
62
+ const EMPTY_RULE = Object.freeze({
63
+ items: Object.freeze([]),
64
+ varFields: Object.freeze({}),
65
+ paramUses: Object.freeze([])
66
+ });
25
67
  /**
26
- * Narrows an aggregate select value a trusted seam over the surface's
27
- * own constructors (only `#query/select.ts` produces `aggregate`-carrying
28
- * values), the same direction as every other constructor-owned shape here.
68
+ * The ordered matchable fields of an atom ownerthe 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.
29
75
  */
30
- function isAggregateEntry(value) {
31
- return "aggregate" in value;
76
+ function matchFieldsOf(owner) {
77
+ if ("axioms" in owner) {
78
+ return [Object.freeze({ name: "id", field: owner.id }), ...owner.data.columns];
79
+ }
80
+ return owner.data.fields;
32
81
  }
33
- /** Narrows a `duration()` select value (only `duration` produces `measure`). */
34
- function isMeasureEntry(value) {
35
- return "measure" in value;
82
+ /**
83
+ * Resolves a bindings record against an atom owner's matchable fields (a
84
+ * relation's declared fields; a closed relation's sealed id + columns), in
85
+ * the record's written order: terms classify by their runtime tag,
86
+ * everything else is a bare literal (typed by the FIELD at lowering — the
87
+ * membership typing rule included). Every bound field carries its
88
+ * law-computed class, read off the schema value's frozen class map — the
89
+ * runtime twin of the type tier's `SlotAt` lookups.
90
+ */
91
+ function resolveBindings(context, relation, bindings, classes) {
92
+ const entries = [];
93
+ const vars = [];
94
+ const uses = [];
95
+ const relationClasses = classes[relation.name];
96
+ const ordered = matchFieldsOf(relation);
97
+ for (const [fieldName, value] of Object.entries(bindings)) {
98
+ if (value === undefined) {
99
+ continue;
100
+ }
101
+ const declared = ordered.find(function byName(candidate) {
102
+ return candidate.name === fieldName;
103
+ });
104
+ if (declared === undefined) {
105
+ throw errors.new(`${context} has no field ${fieldName}`);
106
+ }
107
+ const fieldClass = relationClasses?.[fieldName];
108
+ let bound;
109
+ if (isTerm(value)) {
110
+ switch (value[term]) {
111
+ case "var": {
112
+ bound = Object.freeze({ kind: "var", name: value.name });
113
+ vars.push(Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) }));
114
+ break;
115
+ }
116
+ case "param": {
117
+ bound = Object.freeze({ kind: "param", name: value.name });
118
+ uses.push(Object.freeze({ name: value.name, shape: "value", anchor: declared.field, op: "binding" }));
119
+ break;
120
+ }
121
+ case "setParam": {
122
+ bound = Object.freeze({ kind: "setParam", name: value.name });
123
+ uses.push(Object.freeze({ name: value.name, shape: "set", anchor: declared.field, op: "binding" }));
124
+ break;
125
+ }
126
+ case "maskParam":
127
+ throw errors.new(`${context}.${fieldName}: an Allen-mask param is not a field-typed value — masks live in allen() conditions only`);
128
+ case "duration":
129
+ throw errors.new(`${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`);
130
+ }
131
+ }
132
+ else {
133
+ bound = Object.freeze({ kind: "literal", value });
134
+ }
135
+ entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }));
136
+ }
137
+ return {
138
+ atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
139
+ vars,
140
+ uses
141
+ };
36
142
  }
37
- /** Classifies one select record value into its runtime entry. */
38
- function selectEntryOf(name, value) {
39
- if (isTerm(value)) {
40
- if (value[term] !== "var") {
41
- throw errors.new(`query select column ${name}: a ${value[term]} is not projectable select takes variables, duration(v), or aggregates`);
143
+ /**
144
+ * Extends a rule state with one positive atom. Vars bind on first
145
+ * occurrence; every LATER occurrence (a later atom's field or a same-record
146
+ * sibling) is a join and must be class-equal — the construction-time twin
147
+ * of the type tier's `JoinOk` (bare pairs only with bare), so the domain
148
+ * wall holds for untyped callers too.
149
+ */
150
+ function advanceMatch(state, relation, bindings, classes) {
151
+ const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes);
152
+ const varFields = { ...state.varFields };
153
+ for (const bound of resolved.vars) {
154
+ const existing = varFields[bound.name];
155
+ if (existing === undefined) {
156
+ varFields[bound.name] = bound.slot;
157
+ }
158
+ else if (!fieldJoins(existing, bound.slot)) {
159
+ 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)`);
42
160
  }
43
- return Object.freeze({ kind: "var", over: value });
44
161
  }
45
- if (typeof value === "object" && value !== null) {
46
- if (isAggregateEntry(value)) {
47
- return Object.freeze({ kind: "aggregate", aggregate: value.aggregate });
162
+ return {
163
+ items: Object.freeze([...state.items, Object.freeze({ kind: "atom", atom: resolved.atom })]),
164
+ varFields: Object.freeze(varFields),
165
+ paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
166
+ };
167
+ }
168
+ /** Resolves one comparison side to its runtime term. */
169
+ function cmpTermDataOf(op, value) {
170
+ if (isTerm(value)) {
171
+ switch (value[term]) {
172
+ case "var":
173
+ return Object.freeze({ kind: "var", name: value.name });
174
+ case "param":
175
+ return Object.freeze({ kind: "param", name: value.name });
176
+ case "setParam":
177
+ return Object.freeze({ kind: "setParam", name: value.name });
178
+ case "duration":
179
+ return Object.freeze({ kind: "measure", name: value.name });
180
+ case "maskParam":
181
+ throw errors.new(`${op}: an Allen-mask param is not a comparison term — masks live in allen()'s mask position`);
48
182
  }
49
- if (isMeasureEntry(value)) {
50
- return Object.freeze({ kind: "measure", over: value.measure });
183
+ }
184
+ return Object.freeze({ kind: "literal", value });
185
+ }
186
+ /**
187
+ * One comparison side's contribution to the param census: a param/set side
188
+ * anchors to its SIBLING — a bound variable's field descriptor or the
189
+ * measure; an unanchorable use (literal or param sibling) records with no
190
+ * anchor and must be anchored by some other use of the same name.
191
+ */
192
+ function sideUses(op, side, sibling, varFields, uses) {
193
+ if (side.kind !== "param" && side.kind !== "setParam") {
194
+ return;
195
+ }
196
+ let anchor;
197
+ if (sibling.kind === "var") {
198
+ anchor = varFields[sibling.name]?.field;
199
+ }
200
+ else if (sibling.kind === "measure") {
201
+ anchor = "measure";
202
+ }
203
+ else {
204
+ anchor = undefined;
205
+ }
206
+ uses.push(Object.freeze({
207
+ name: side.name,
208
+ shape: side.kind === "param" ? "value" : "set",
209
+ anchor,
210
+ op
211
+ }));
212
+ }
213
+ /** Lowers one condition VALUE to its runtime data, recording param uses. */
214
+ function condDataOf(cond, varFields, uses) {
215
+ if (cond.cond === "cmp") {
216
+ const lhs = cmpTermDataOf(cond.op, cond.lhs);
217
+ const rhs = cmpTermDataOf(cond.op, cond.rhs);
218
+ sideUses(cond.op, lhs, rhs, varFields, uses);
219
+ sideUses(cond.op, rhs, lhs, varFields, uses);
220
+ let mask;
221
+ if (cond.op === "allen") {
222
+ const maskValue = cond.mask;
223
+ if (typeof maskValue === "number") {
224
+ mask = Object.freeze({ kind: "literal", mask: maskValue });
225
+ }
226
+ else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
227
+ mask = Object.freeze({ kind: "param", name: maskValue.name });
228
+ uses.push(Object.freeze({ name: maskValue.name, shape: "mask", anchor: undefined, op: "allen" }));
229
+ }
230
+ else {
231
+ throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam");
232
+ }
51
233
  }
234
+ const data = Object.freeze({ kind: "cmp", op: cond.op, mask, lhs, rhs });
235
+ return data;
236
+ }
237
+ if (cond.cond === "tree") {
238
+ const children = cond.children.map(function lowerChild(child) {
239
+ return condDataOf(child, varFields, uses);
240
+ });
241
+ const data = Object.freeze({ kind: "tree", op: cond.op, children: Object.freeze(children) });
242
+ return data;
52
243
  }
53
- throw errors.new(`query select column ${name}: not a select entryselect takes variables, duration(v), or aggregates`);
244
+ throw errors.new("a negated atom is not a condition-tree nodepass not(...) to where() directly, never inside and()/or()");
54
245
  }
55
- /** Renders an atom source's name for construction diagnostics. */
56
- function sourceName(source) {
57
- if (source.kind === "relation") {
58
- return source.relation.name;
246
+ /** Extends a rule state with one `.where` item (a condition or a negated atom). */
247
+ function advanceWhere(state, cond, classes) {
248
+ if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
249
+ throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom");
59
250
  }
60
- return `predicate ${source.pred.name}`;
251
+ if (cond.cond === "not") {
252
+ const relation = cond.relation;
253
+ const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
254
+ return value !== undefined;
255
+ }));
256
+ const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes);
257
+ return {
258
+ items: Object.freeze([...state.items, Object.freeze({ kind: "negated", atom: resolved.atom })]),
259
+ varFields: state.varFields,
260
+ paramUses: Object.freeze([...state.paramUses, ...resolved.uses])
261
+ };
262
+ }
263
+ const uses = [];
264
+ const data = condDataOf(cond, state.varFields, uses);
265
+ return {
266
+ items: Object.freeze([...state.items, Object.freeze({ kind: "cond", cond: data })]),
267
+ varFields: state.varFields,
268
+ paramUses: Object.freeze([...state.paramUses, ...uses])
269
+ };
61
270
  }
62
- /** Collects every variable a rule's positive atoms bind. */
63
- function positiveVarsOf(body) {
64
- const positive = new Set();
65
- for (const item of body) {
66
- if (item.item !== "atom" || item.negated) {
67
- continue;
271
+ /** Extends a rule state with one `idb` atom (vars must be bound — validated at completion). */
272
+ function advanceIdb(state, rec, vars) {
273
+ const names = vars.map(function nameOf(variable) {
274
+ if (!isTerm(variable) || variable[term] !== "var") {
275
+ throw errors.new(`idb ${rec.name}: positions take variables — bind literals and params through where()/match()`);
68
276
  }
69
- for (const binding of item.bindings) {
70
- if (binding.term.kind === "term" && binding.term.value[term] === "var") {
71
- positive.add(binding.term.value);
72
- }
277
+ return variable.name;
278
+ });
279
+ return {
280
+ items: Object.freeze([...state.items, Object.freeze({ kind: "idb", rec, vars: Object.freeze(names) })]),
281
+ varFields: state.varFields,
282
+ paramUses: state.paramUses
283
+ };
284
+ }
285
+ /** Narrows a select entry to an aggregate value. */
286
+ function isAggregateEntry(value) {
287
+ return typeof value === "object" && value !== null && "agg" in value;
288
+ }
289
+ /** Classifies one select entry into its named answer column. */
290
+ function selectColumnOf(entry) {
291
+ if (typeof entry === "string") {
292
+ return Object.freeze({ name: entry, entry: Object.freeze({ kind: "var", over: entry }) });
293
+ }
294
+ if (isTerm(entry)) {
295
+ if (entry[term] === "duration") {
296
+ return Object.freeze({ name: entry.name, entry: Object.freeze({ kind: "measure", over: entry.name }) });
73
297
  }
298
+ throw errors.new(`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`);
74
299
  }
75
- return positive;
300
+ if (isAggregateEntry(entry)) {
301
+ return aggregateColumnOf(entry);
302
+ }
303
+ throw errors.new("query select: not a select entry — select takes variable names, duration(v), or aggregates");
76
304
  }
77
- /** Judges one negated atom against the rule's positively-bound variables. */
78
- function assertNegatedAtomSafe(context, positive, atom) {
79
- for (const binding of atom.bindings) {
80
- if (binding.term.kind === "oneOf") {
81
- throw errors.new(`${context}: negated ${sourceName(atom.source)} atom binds ${binding.field} with oneOf(...) — its lowering mints a variable no positive atom binds (the safety rule); write one negated atom per literal, or bind a paramSet`);
305
+ /** Classifies one aggregate select entry. */
306
+ function aggregateColumnOf(entry) {
307
+ function column(name, agg) {
308
+ return Object.freeze({ name, entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }) });
309
+ }
310
+ const over = entry.over;
311
+ switch (entry.agg) {
312
+ case "count":
313
+ return column("count", { op: "count" });
314
+ case "countDistinct": {
315
+ if (typeof over !== "string") {
316
+ throw errors.new("countDistinct takes a variable name");
317
+ }
318
+ return column(over, { op: "countDistinct", over });
82
319
  }
83
- if (binding.term.kind !== "term" || binding.term.value[term] !== "var") {
84
- continue;
320
+ case "sum":
321
+ case "min":
322
+ case "max": {
323
+ if (typeof over === "string") {
324
+ return column(over, { op: "fold", fold: entry.agg, over });
325
+ }
326
+ if (isTerm(over) && over[term] === "duration") {
327
+ return column(over.name, { op: "fold", fold: entry.agg, over: Object.freeze({ duration: over.name }) });
328
+ }
329
+ throw errors.new(`${entry.agg} takes a variable name or duration(v)`);
330
+ }
331
+ case "argMax":
332
+ case "argMin": {
333
+ if (typeof over !== "string" || typeof entry.key !== "string") {
334
+ throw errors.new(`${entry.agg} takes a carried variable name and an orderable key variable name`);
335
+ }
336
+ return column(over, { op: "arg", direction: entry.agg, over, key: entry.key });
85
337
  }
86
- const variable = binding.term.value;
87
- if (!positive.has(variable)) {
88
- throw errors.new(`${context}: negated ${sourceName(atom.source)} atom binds the variable declared from ${variable.relation}.${variable.field} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`);
338
+ case "pack": {
339
+ if (typeof over !== "string") {
340
+ throw errors.new("pack takes a variable name");
341
+ }
342
+ return column(over, { op: "pack", over });
89
343
  }
344
+ default:
345
+ throw errors.new(`unknown aggregate ${entry.agg}`);
346
+ }
347
+ }
348
+ /** Requires a var name to be bound by a relation atom of the rule. */
349
+ function assertBound(context, varFields, name) {
350
+ const slot = varFields[name];
351
+ if (slot === undefined) {
352
+ throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`);
353
+ }
354
+ return slot;
355
+ }
356
+ /** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
357
+ function assertIntervalBound(context, varFields, name) {
358
+ const slot = assertBound(context, varFields, name);
359
+ if (slot.field.kind !== "interval") {
360
+ throw errors.new(`${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`);
90
361
  }
91
362
  }
92
363
  /**
93
- * The negation safety rule, judged at construction (earlier and warmer
94
- * than the engine's refusal, which also stands): every variable a negated
95
- * atom uses must be bound by a positive atom of the same rule a negated
96
- * atom binds nothing, only rejects.
364
+ * Validates one condition's variable references against the rule's bound
365
+ * names and, for `eq`/`ne` over two variables, holds the class wall: the
366
+ * unification IS a join, so the two slots must be class-equal exactly as a
367
+ * match-reuse join must be (the construction-time twin of the type tier's
368
+ * `EqOk` → `JoinOk`; bare pairs only with bare). The engine cannot backstop
369
+ * this one — the query IR carries no domains — so the wall lives here for
370
+ * untyped callers too.
97
371
  */
98
- function assertNegationSafety(context, body) {
99
- const positive = positiveVarsOf(body);
100
- for (const item of body) {
101
- if (item.item === "atom" && item.negated) {
102
- assertNegatedAtomSafe(context, positive, item);
372
+ function validateCond(context, varFields, cond) {
373
+ if (cond.kind === "cmp") {
374
+ for (const side of [cond.lhs, cond.rhs]) {
375
+ if (side.kind === "var") {
376
+ assertBound(context, varFields, side.name);
377
+ }
378
+ if (side.kind === "measure") {
379
+ assertIntervalBound(context, varFields, side.name);
380
+ }
103
381
  }
382
+ if ((cond.op === "eq" || cond.op === "ne") && cond.lhs.kind === "var" && cond.rhs.kind === "var") {
383
+ const lhs = assertBound(context, varFields, cond.lhs.name);
384
+ const rhs = assertBound(context, varFields, cond.rhs.name);
385
+ if (!fieldJoins(lhs, rhs)) {
386
+ 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)`);
387
+ }
388
+ }
389
+ return;
390
+ }
391
+ for (const child of cond.children) {
392
+ validateCond(context, varFields, child);
393
+ }
394
+ }
395
+ /** Validates one select column's variable references. */
396
+ function validateColumn(context, varFields, column) {
397
+ const entry = column.entry;
398
+ if (entry.kind === "var") {
399
+ assertBound(`${context} select ${column.name}`, varFields, entry.over);
400
+ return;
401
+ }
402
+ if (entry.kind === "measure") {
403
+ assertIntervalBound(`${context} select ${column.name}`, varFields, entry.over);
404
+ return;
405
+ }
406
+ const agg = entry.agg;
407
+ switch (agg.op) {
408
+ case "count":
409
+ return;
410
+ case "countDistinct":
411
+ assertBound(`${context} select ${column.name}`, varFields, agg.over);
412
+ return;
413
+ case "fold": {
414
+ if (typeof agg.over === "string") {
415
+ assertBound(`${context} select ${column.name}`, varFields, agg.over);
416
+ return;
417
+ }
418
+ assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration);
419
+ return;
420
+ }
421
+ case "arg":
422
+ assertBound(`${context} select ${column.name}`, varFields, agg.over);
423
+ assertBound(`${context} select ${column.name}`, varFields, agg.key);
424
+ return;
425
+ case "pack":
426
+ assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over);
427
+ return;
104
428
  }
105
429
  }
106
430
  /**
107
- * Builds a query as a typed value: runs `build` inside a fresh scope,
108
- * classifies the select record (written order = answer column order),
109
- * validates negation safety across every rule (the predicates' rules
110
- * included), and freezes. Rule counts, strata legality, and every deeper
111
- * roster stay the engine's judge at prepare.
431
+ * Completes one rule: classifies the select record (written order = answer
432
+ * column order, names must be declaration-order-safe keys), and validates
433
+ * boundness every condition/select/idb variable bound by a relation atom,
434
+ * and every NEGATED atom's variable positively bound (the safety rule: a
435
+ * negated atom binds nothing, only rejects).
112
436
  */
113
- function query(theory, build) {
114
- const registry = createRegistry(theory);
115
- const scope = Object.freeze({
116
- var(field) {
117
- return scopeVar(registry, field);
118
- },
119
- param(name, field) {
120
- return scopeParam(registry, name, field);
437
+ function completeRule(context, state, columns) {
438
+ if (columns.length === 0) {
439
+ throw errors.new(`${context}: a select needs at least one entry`);
440
+ }
441
+ const seen = new Set();
442
+ for (const column of columns) {
443
+ assertDeclarationOrderKey(`${context} select column`, column.name);
444
+ if (seen.has(column.name)) {
445
+ throw errors.new(`${context}: select names the answer column ${column.name} twice`);
446
+ }
447
+ seen.add(column.name);
448
+ validateColumn(context, state.varFields, column);
449
+ }
450
+ for (const item of state.items) {
451
+ if (item.kind === "negated") {
452
+ for (const binding of item.atom.bindings) {
453
+ if (binding.term.kind === "var") {
454
+ const bound = state.varFields[binding.term.name];
455
+ if (bound === undefined) {
456
+ 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)`);
457
+ }
458
+ const negatedSlot = { field: binding.data, class: binding.class };
459
+ if (!fieldJoins(bound, negatedSlot)) {
460
+ 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`);
461
+ }
462
+ }
463
+ }
464
+ }
465
+ if (item.kind === "idb") {
466
+ const head = item.rec.rules[0];
467
+ item.vars.forEach(function checkIdbVar(name, position) {
468
+ const bound = state.varFields[name];
469
+ if (bound === undefined) {
470
+ 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`);
471
+ }
472
+ const column = head?.select[position];
473
+ if (column === undefined || column.entry.kind !== "var") {
474
+ return;
475
+ }
476
+ const headSlot = head?.varFields[column.entry.over];
477
+ if (headSlot !== undefined && !fieldJoins(headSlot, bound)) {
478
+ 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`);
479
+ }
480
+ });
481
+ }
482
+ if (item.kind === "cond") {
483
+ validateCond(context, state.varFields, item.cond);
484
+ }
485
+ }
486
+ return Object.freeze({
487
+ items: state.items,
488
+ select: Object.freeze([...columns]),
489
+ varFields: state.varFields,
490
+ paramUses: state.paramUses
491
+ });
492
+ }
493
+ /** Builds one typed rule value over completed rule data. */
494
+ function makeRuleValue(rule) {
495
+ return Object.freeze({ rule });
496
+ }
497
+ /** The diagnostic label of a chain context. */
498
+ function contextLabel(context) {
499
+ switch (context.kind) {
500
+ case "query":
501
+ return "query rule";
502
+ case "rec":
503
+ return `rec ${context.self.name} rule`;
504
+ case "output":
505
+ return "program output rule";
506
+ }
507
+ }
508
+ /** Validates and records one `idb` atom per the context's cut. */
509
+ function idbAdvance(context, state, target, vars) {
510
+ if (context.kind === "query") {
511
+ throw errors.new("idb is a program construct — declare recs and outputs through program(), never a plain query()");
512
+ }
513
+ if (context.kind === "rec") {
514
+ if (target.data !== context.self) {
515
+ 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)`);
516
+ }
517
+ return advanceIdb(state, context.self, vars);
518
+ }
519
+ if (!context.program.recs.includes(target.data)) {
520
+ throw errors.new(`idb ${target.name}: the rec was declared by a different program — rec identity is the membership rule`);
521
+ }
522
+ return advanceIdb(state, target.data, vars);
523
+ }
524
+ /** Classifies one select tuple per the context (a recursive head projects bound NAMES only). */
525
+ function selectColumns(context, entries) {
526
+ return entries.map(function columnOf(entry) {
527
+ if (context.kind === "rec" && typeof entry !== "string") {
528
+ 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)`);
529
+ }
530
+ return selectColumnOf(entry);
531
+ });
532
+ }
533
+ /** Builds one runtime chain (immutably — every step is a fresh chain over fresh state). */
534
+ function makeRawChain(context, state) {
535
+ const chain = {
536
+ match(relation, bindings) {
537
+ return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes));
121
538
  },
122
- paramSet(name, field) {
123
- return scopeParamSet(registry, name, field);
539
+ where(cond) {
540
+ return makeRawChain(context, advanceWhere(state, cond, context.classes));
124
541
  },
125
- allenParam(name) {
126
- return scopeAllenParam(registry, name);
542
+ idb(target, ...vars) {
543
+ return makeRawChain(context, idbAdvance(context, state, target, vars));
127
544
  },
128
- predicate(name, columns, rules) {
129
- return makePredicate(registry, name, columns, rules);
545
+ select(...entries) {
546
+ return makeRuleValue(completeRule(contextLabel(context), state, selectColumns(context, entries)));
130
547
  }
131
- });
132
- const built = build(scope);
133
- const select = [];
134
- for (const [name, value] of Object.entries(built.select)) {
135
- assertDeclarationOrderKey("query select column", name);
136
- select.push(Object.freeze({ name, entry: selectEntryOf(name, value) }));
137
- }
138
- for (const pred of registry.predicates) {
139
- pred.rules.forEach(function validateRule(rule, index) {
140
- assertNegationSafety(`query construction (predicate ${pred.name}, rule ${index})`, rule.body);
141
- });
548
+ };
549
+ Object.freeze(chain);
550
+ return chain;
551
+ }
552
+ /** Builds one runtime rule-builder over a context. */
553
+ function makeRawScope(context) {
554
+ const scope = {
555
+ ...termOps,
556
+ match(relation, bindings) {
557
+ return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes));
558
+ }
559
+ };
560
+ Object.freeze(scope);
561
+ return scope;
562
+ }
563
+ /**
564
+ * The one trusted admission seam of the rule builders (the `relation()`
565
+ * `refsComplete` pattern): the raw builder is one runtime shape for every
566
+ * context, and this guard verifies the checkable fact — the builder verbs
567
+ * exist — before the value is admitted at its TYPED face. The type-level
568
+ * judgments (domain-equal joins, boundness, the recursion cut) live in the
569
+ * interfaces themselves; the runtime twin of every one of them is a
570
+ * construction-time validation in this module.
571
+ */
572
+ function isTypedScope(scope) {
573
+ return typeof scope.match === "function";
574
+ }
575
+ /** Builds one query-rule builder (the typed face of the raw builder). */
576
+ function makeQueryRuleScope(classes) {
577
+ const raw = makeRawScope({ kind: "query", classes });
578
+ if (!isTypedScope(raw)) {
579
+ throw errors.new("query rule builder construction incomplete");
580
+ }
581
+ return raw;
582
+ }
583
+ /** Builds one output-rule builder over a program's recs. */
584
+ function makeOutputRuleScope(program) {
585
+ const raw = makeRawScope({ kind: "output", program, classes: program.classes });
586
+ if (!isTypedScope(raw)) {
587
+ throw errors.new("program output rule builder construction incomplete");
142
588
  }
143
- const rules = built.rules.map(function freezeRule(rule, index) {
144
- assertNegationSafety(`query construction (rule ${index})`, rule);
145
- return Object.freeze([...rule]);
589
+ return raw;
590
+ }
591
+ /** Renders one head column's signature for the rule-alignment check. */
592
+ function headSignature(column) {
593
+ const entry = column.entry;
594
+ if (entry.kind === "var" || entry.kind === "measure") {
595
+ return `${column.name}:var`;
596
+ }
597
+ const agg = entry.agg;
598
+ if (agg.op === "fold") {
599
+ return `${column.name}:${agg.fold}`;
600
+ }
601
+ if (agg.op === "arg") {
602
+ return `${column.name}:${agg.direction}`;
603
+ }
604
+ return `${column.name}:${agg.op}`;
605
+ }
606
+ /**
607
+ * Folds every rule's param uses (recs in declaration order first, output
608
+ * rules last — exactly the lowering walk) into the query's registry:
609
+ * first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
610
+ * the wire, and one name must keep one shape.
611
+ */
612
+ function paramRegistryOf(recs, rules) {
613
+ const order = [];
614
+ const byName = new Map();
615
+ function fold(uses) {
616
+ for (const use of uses) {
617
+ const existing = byName.get(use.name);
618
+ if (existing === undefined) {
619
+ order.push(use.name);
620
+ byName.set(use.name, { shape: use.shape, anchor: use.anchor, op: use.op });
621
+ continue;
622
+ }
623
+ if (existing.shape !== use.shape) {
624
+ throw errors.new(`query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`);
625
+ }
626
+ if (existing.anchor === undefined && use.anchor !== undefined) {
627
+ existing.anchor = use.anchor;
628
+ existing.op = use.op;
629
+ }
630
+ }
631
+ }
632
+ for (const rec of recs) {
633
+ for (const rule of rec.rules) {
634
+ fold(rule.paramUses);
635
+ }
636
+ }
637
+ for (const rule of rules) {
638
+ fold(rule.paramUses);
639
+ }
640
+ return Object.freeze(order.map(function entryOf(name) {
641
+ const entry = byName.get(name);
642
+ if (entry === undefined) {
643
+ throw errors.new(`query param ${name} lost its registry entry`);
644
+ }
645
+ return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op });
646
+ }));
647
+ }
648
+ /**
649
+ * Assembles the runtime query value over completed rules: every rule must
650
+ * derive the SAME head (name and aggregate shape, position for position —
651
+ * the decode labels and the engine's alignment rule agree by
652
+ * construction), and the param registry folds in program-walk order.
653
+ */
654
+ function makeRawQuery(theory, recs, rules) {
655
+ const first = rules[0];
656
+ if (first === undefined) {
657
+ throw errors.new("a query needs at least one rule");
658
+ }
659
+ const signature = first.select.map(headSignature).join(", ");
660
+ rules.forEach(function verifyHead(rule, index) {
661
+ const candidate = rule.select.map(headSignature).join(", ");
662
+ if (candidate !== signature) {
663
+ throw errors.new(`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`);
664
+ }
146
665
  });
147
- Object.freeze(registry.params);
148
- Object.freeze(registry.predicates);
149
- Object.freeze(registry);
150
- return Object.freeze({
151
- schema: theory,
152
- data: Object.freeze({ registry, rules: Object.freeze(rules), select: Object.freeze(select) })
666
+ const data = Object.freeze({
667
+ recs: Object.freeze([...recs]),
668
+ rules: Object.freeze([...rules]),
669
+ select: first.select,
670
+ params: paramRegistryOf(recs, rules)
153
671
  });
672
+ const value = {
673
+ schema: theory,
674
+ data,
675
+ rule(build) {
676
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes }));
677
+ return makeRawQuery(theory, recs, [...rules, built.rule]);
678
+ }
679
+ };
680
+ Object.freeze(value);
681
+ return value;
682
+ }
683
+ /**
684
+ * The query values' trusted admission seam (the `refsComplete` pattern):
685
+ * the checkable fact — the value was assembled over the identical theory —
686
+ * is verified before the raw value is admitted at its typed face.
687
+ */
688
+ function isQueryValue(theory, value) {
689
+ return value.schema === theory;
690
+ }
691
+ /** Assembles one typed query value (rules already completed). */
692
+ function makeQuery(theory, recs, rules) {
693
+ const raw = makeRawQuery(theory, recs, rules);
694
+ if (!isQueryValue(theory, raw)) {
695
+ throw errors.new("query value construction incomplete");
696
+ }
697
+ return raw;
698
+ }
699
+ /**
700
+ * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
701
+ * adds one conjunctive rule; multiple rules are the set union (answers are
702
+ * SETS — no order or limit exists anywhere; hosts sort). The schema's
703
+ * law-computed class map rides into every rule builder — the join walls
704
+ * compare class names off it, at the type level and at construction alike.
705
+ */
706
+ function query(theory) {
707
+ const start = {
708
+ rule(build) {
709
+ const built = build(makeQueryRuleScope(theory.classes));
710
+ return makeQuery(theory, [], [built.rule]);
711
+ }
712
+ };
713
+ Object.freeze(start);
714
+ return start;
154
715
  }
155
716
  /** The typed shape refusal of the literal tagger — a genuine failure, never data. */
156
717
  function literalShapeError(context, expected, value) {
@@ -165,20 +726,54 @@ function isIntervalShaped(value) {
165
726
  typeof value.start === "bigint" &&
166
727
  typeof value.end === "bigint");
167
728
  }
729
+ /**
730
+ * Tags one closed-reference literal: the bare handle id, verified against
731
+ * the roster (the belt the type level cannot provide — structural values
732
+ * make any bigint spellable here) and tagged u64 — queries cross ids,
733
+ * never handle names.
734
+ */
735
+ function taggedHandleId(context, closed, value) {
736
+ if (typeof value !== "bigint") {
737
+ throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value);
738
+ }
739
+ if (closed.handles[Number(value)] === undefined) {
740
+ throw errors.new(`${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`);
741
+ }
742
+ return { kind: "u64", value };
743
+ }
744
+ /**
745
+ * Tags one literal in an interval element domain: a bigint tags as the
746
+ * element (the membership typing rule's point side), an interval-shaped
747
+ * value as the interval (value equality).
748
+ */
749
+ function taggedAtElementDomain(context, element, value) {
750
+ if (typeof value === "bigint") {
751
+ if (element === "u64") {
752
+ return { kind: "u64", value };
753
+ }
754
+ return { kind: "i64", value };
755
+ }
756
+ if (isIntervalShaped(value)) {
757
+ if (element === "u64") {
758
+ return { kind: "intervalU64", start: value.start, end: value.end };
759
+ }
760
+ return { kind: "intervalI64", start: value.start, end: value.end };
761
+ }
762
+ throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value);
763
+ }
168
764
  /**
169
765
  * Tags one host literal at a FIELD position (atom bindings): the field's
170
- * structural type directs the tag, never a guess. At an interval field a
766
+ * structural kind directs the tag, never a guess. At an interval field a
171
767
  * bigint literal tags as the ELEMENT type — the IR's membership typing
172
768
  * rule (point membership), an interval-shaped literal as the interval
173
- * (value equality). A closed-reference literal is its branded id, tagged
174
- * u64 after a roster re-verification (queries cross ids, never handle
175
- * names).
769
+ * (value equality). A closed-reference literal is its bare handle id,
770
+ * tagged u64 after a roster verification.
176
771
  */
177
772
  function taggedLiteral(context, field, value) {
178
- if (field.closed !== undefined) {
773
+ if ("closed" in field) {
179
774
  return taggedHandleId(context, field.closed, value);
180
775
  }
181
- switch (field.type.kind) {
776
+ switch (field.kind) {
182
777
  case "bool": {
183
778
  if (typeof value !== "boolean") {
184
779
  throw literalShapeError(context, "boolean", value);
@@ -197,68 +792,34 @@ function taggedLiteral(context, field, value) {
197
792
  }
198
793
  return { kind: "i64", value };
199
794
  }
200
- case "string": {
795
+ case "str": {
201
796
  if (typeof value !== "string") {
202
797
  throw literalShapeError(context, "string", value);
203
798
  }
204
799
  return { kind: "string", value };
205
800
  }
206
- case "fixedBytes": {
801
+ case "bytes": {
207
802
  if (!(value instanceof Uint8Array)) {
208
803
  throw literalShapeError(context, "Uint8Array", value);
209
804
  }
210
805
  return { kind: "fixedBytes", value };
211
806
  }
212
807
  case "interval":
213
- return taggedAtElementDomain(context, field.type.element, value);
808
+ return taggedAtElementDomain(context, field.element, value);
214
809
  }
215
810
  }
216
811
  /**
217
- * Tags one closed-reference literal: the branded id, re-verified against
218
- * the roster (the belt the type level cannot provide against forged
219
- * brands) and tagged u64 queries cross ids, never handle names.
220
- */
221
- function taggedHandleId(context, closed, value) {
222
- if (typeof value !== "bigint") {
223
- throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value);
224
- }
225
- if (closed.handles[Number(value)] === undefined) {
226
- throw errors.new(`${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`);
227
- }
228
- return { kind: "u64", value };
229
- }
230
- /**
231
- * Tags one literal in an interval element domain: a bigint tags as the
232
- * element (the membership typing rule's point side), an interval-shaped
233
- * value as the interval (value equality).
234
- */
235
- function taggedAtElementDomain(context, element, value) {
236
- if (typeof value === "bigint") {
237
- if (element === "u64") {
238
- return { kind: "u64", value };
239
- }
240
- return { kind: "i64", value };
241
- }
242
- if (isIntervalShaped(value)) {
243
- if (element === "u64") {
244
- return { kind: "intervalU64", start: value.start, end: value.end };
245
- }
246
- return { kind: "intervalI64", start: value.start, end: value.end };
247
- }
248
- throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value);
249
- }
250
- /**
251
- * Tags one host literal at a COMPARISON position, where no field position
252
- * directs the type: the sibling term's element domain does — a measure
253
- * sibling is u64, an interval-field sibling contributes its element type
254
- * (so both a point literal in `covers` and a `span` literal in `allen`
255
- * tag correctly), a scalar sibling its own type. At `pointIn` the operand
256
- * order is interval-left, point-right (`ir::CmpOp::PointIn`), so an
257
- * interval-shaped literal beside a scalar element-typed sibling is the
258
- * LEGAL lhs of `covers(span(...), t)` and tags as the interval of the
259
- * sibling's element domain; under every other operator an interval shape
260
- * against a scalar sibling stays refused (the engine's IllegalComparison
261
- * — eq/lt interval-vs-scalar is not a comparison).
812
+ * Tags one host literal at a COMPARISON or PARAM position, where the
813
+ * SIBLING anchors the type: a measure sibling is u64, an interval-field
814
+ * sibling contributes its element domain (so both a point literal in
815
+ * `covers` and a `span` literal in `allen` tag correctly), a scalar
816
+ * sibling its own type. At `pointIn` the operand order is interval-left,
817
+ * point-right (`ir::CmpOp::PointIn`), so an interval-shaped literal
818
+ * beside a scalar element-typed sibling is the LEGAL lhs of
819
+ * `covers(span(...), t)` and tags as the interval of the sibling's
820
+ * element domain; under every other operator an interval shape against a
821
+ * scalar sibling stays refused (the engine's IllegalComparison the
822
+ * bug-hunt fix, preserved op-aware).
262
823
  */
263
824
  function taggedCmpLiteral(context, sibling, value, op) {
264
825
  if (sibling === "measure") {
@@ -267,195 +828,161 @@ function taggedCmpLiteral(context, sibling, value, op) {
267
828
  }
268
829
  return { kind: "u64", value };
269
830
  }
270
- if (sibling.type.kind === "interval") {
271
- return taggedAtElementDomain(context, sibling.type.element, value);
831
+ if (!("closed" in sibling) && sibling.kind === "interval") {
832
+ return taggedAtElementDomain(context, sibling.element, value);
272
833
  }
273
- if (op === "pointIn" && (sibling.type.kind === "u64" || sibling.type.kind === "i64") && isIntervalShaped(value)) {
274
- return taggedAtElementDomain(context, sibling.type.kind, value);
834
+ if (op === "pointIn" &&
835
+ !("closed" in sibling) &&
836
+ (sibling.kind === "u64" || sibling.kind === "i64") &&
837
+ isIntervalShaped(value)) {
838
+ return taggedAtElementDomain(context, sibling.kind, value);
275
839
  }
276
840
  return taggedLiteral(context, sibling, value);
277
841
  }
278
842
  /** Creates one rule-scoped variable numberer. */
279
- function makeVarIds(registry) {
843
+ function makeVarIds() {
280
844
  const assigned = new Map();
281
- const state = { next: 0 };
282
845
  return {
283
- of(variable) {
284
- const existing = assigned.get(variable);
846
+ of(name) {
847
+ const existing = assigned.get(name);
285
848
  if (existing !== undefined) {
286
849
  return existing;
287
850
  }
288
- if (!registry.vars.has(variable)) {
289
- throw errors.new(`query lowering: the variable declared from ${variable.relation}.${variable.field} belongs to a different query scope`);
290
- }
291
- const id = state.next;
292
- state.next += 1;
293
- assigned.set(variable, id);
294
- return id;
295
- },
296
- synthetic() {
297
- const id = state.next;
298
- state.next += 1;
851
+ const id = assigned.size;
852
+ assigned.set(name, id);
299
853
  return id;
300
854
  }
301
855
  };
302
856
  }
303
- /** Resolves a param term to its dense positional id, recording the use. */
304
- function paramIdOf(ctx, value, name) {
305
- const index = ctx.registry.paramIndex.get(value);
306
- if (index === undefined) {
307
- throw errors.new(`query lowering: param ${name} belongs to a different query scope`);
857
+ /** Resolves a param name to its dense positional id. */
858
+ function paramIdOf(ctx, name) {
859
+ const id = ctx.paramIds.get(name);
860
+ if (id === undefined) {
861
+ throw errors.new(`query lowering: param ${name} is not in the query's registry`);
308
862
  }
309
- ctx.usedParams.add(index);
310
- return index;
311
- }
312
- /** Lowers one atom (either polarity) and appends any `oneOf` disjunctions. */
313
- function lowerAtom(ctx, atom, ids, extraConditions) {
314
- const source = lowerSource(ctx, atom.source);
315
- const bindings = atom.bindings.map(function lowerBinding(binding) {
316
- return [
317
- fieldOrdinal(atom.source, binding),
318
- lowerBindingTerm(ctx, atom.source, binding, ids, extraConditions)
319
- ];
320
- });
321
- return { source, bindings };
863
+ return id;
322
864
  }
323
- /** Lowers an atom source to its numeric id, verifying scope membership. */
324
- function lowerSource(ctx, source) {
325
- if (source.kind === "relation") {
326
- const member = ctx.theory.relations[source.relation.name];
327
- if (member !== source.relation) {
328
- throw errors.new(`query lowering: relation ${source.relation.name} is not the relation value schema ${ctx.theory.name} declares`);
329
- }
330
- const id = ctx.relationIds.get(source.relation.name);
331
- if (id === undefined) {
332
- throw errors.new(`query lowering: relation ${source.relation.name} has no ordinal`);
333
- }
334
- return { kind: "edb", relation: id };
865
+ /**
866
+ * Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
867
+ * same edb source its ordinal is its record-declaration slot exactly like
868
+ * an ordinary relation's — with field ordinals over the SEALED shape: `id`
869
+ * at 0, each payload column at its declared index + 1 (`matchFieldsOf`
870
+ * carries the shift; the lowering golden pins it).
871
+ */
872
+ function lowerAtom(ctx, atom, ids) {
873
+ const member = ctx.theory.relations[atom.relation.name];
874
+ if (member !== atom.relation) {
875
+ throw errors.new(`query lowering: relation ${atom.relation.name} is not the relation value schema ${ctx.theory.name} declares`);
335
876
  }
336
- const pred = ctx.predicateIds.get(source.pred);
337
- if (pred === undefined) {
338
- throw errors.new(`query lowering: predicate ${source.pred.name} was declared in a different query scope`);
877
+ const relationId = ctx.relationIds.get(atom.relation.name);
878
+ if (relationId === undefined) {
879
+ throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`);
339
880
  }
340
- return { kind: "idb", pred };
341
- }
342
- /** A binding's field ordinal: declaration index (relations) or head position (predicates). */
343
- function fieldOrdinal(source, binding) {
344
- if (source.kind === "relation") {
345
- const ordinal = source.relation.data.fields.findIndex(function byName(candidate) {
881
+ const ordered = matchFieldsOf(atom.relation);
882
+ const bindings = atom.bindings.map(function lowerBinding(binding) {
883
+ const ordinal = ordered.findIndex(function byName(candidate) {
346
884
  return candidate.name === binding.field;
347
885
  });
348
886
  if (ordinal < 0) {
349
- throw errors.new(`query lowering: relation ${source.relation.name} has no field ${binding.field}`);
887
+ throw errors.new(`query lowering: relation ${atom.relation.name} has no field ${binding.field}`);
350
888
  }
351
- return ordinal;
352
- }
353
- const ordinal = source.pred.columns.findIndex(function byName(candidate) {
354
- return candidate.name === binding.field;
889
+ return [ordinal, lowerBindingTerm(ctx, `${atom.relation.name}.${binding.field}`, binding, ids)];
355
890
  });
356
- if (ordinal < 0) {
357
- throw errors.new(`query lowering: predicate ${source.pred.name} has no column ${binding.field}`);
358
- }
359
- return ordinal;
891
+ return { source: { kind: "edb", relation: relationId }, bindings };
360
892
  }
361
- /** Lowers one binding term; an `oneOf` mints a fresh variable + disjunction. */
362
- function lowerBindingTerm(ctx, source, binding, ids, extraConditions) {
363
- const context = `${sourceName(source)}.${binding.field}`;
893
+ /** Lowers one binding term. */
894
+ function lowerBindingTerm(ctx, context, binding, ids) {
364
895
  const bound = binding.term;
365
- if (bound.kind === "term") {
366
- const value = bound.value;
367
- switch (value[term]) {
368
- case "var":
369
- return { kind: "var", var: ids.of(value) };
370
- case "param":
371
- return { kind: "param", param: paramIdOf(ctx, value, value.name) };
372
- case "paramSet":
373
- return { kind: "paramSet", param: paramIdOf(ctx, value, value.name) };
374
- case "maskParam":
375
- throw errors.new(`${context}: an Allen-mask param is not a field-typed binding`);
376
- }
377
- }
378
- if (bound.kind === "oneOf") {
379
- const minted = ids.synthetic();
380
- const leaves = bound.values.map(function equalityLeaf(candidate) {
381
- return {
382
- kind: "leaf",
383
- cmp: {
384
- op: { kind: "eq" },
385
- lhs: { kind: "var", var: minted },
386
- rhs: { kind: "literal", value: taggedLiteral(context, binding.data, candidate) }
387
- }
388
- };
389
- });
390
- extraConditions.push({ kind: "or", children: leaves });
391
- return { kind: "var", var: minted };
896
+ switch (bound.kind) {
897
+ case "var":
898
+ return { kind: "var", var: ids.of(bound.name) };
899
+ case "param":
900
+ return { kind: "param", param: paramIdOf(ctx, bound.name) };
901
+ case "setParam":
902
+ return { kind: "paramSet", param: paramIdOf(ctx, bound.name) };
903
+ case "literal":
904
+ return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
392
905
  }
393
- return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
394
906
  }
395
- /** Lowers one comparison side; literals tag by the sibling's element domain (op-aware at `pointIn`). */
396
- function lowerCmpTerm(ctx, side, sibling, ids, op) {
397
- if (side.kind === "term") {
398
- const value = side.value;
399
- switch (value[term]) {
400
- case "var":
401
- return { kind: "var", var: ids.of(value) };
402
- case "param":
403
- return { kind: "param", param: paramIdOf(ctx, value, value.name) };
404
- case "paramSet":
405
- return { kind: "paramSet", param: paramIdOf(ctx, value, value.name) };
406
- case "maskParam":
407
- throw errors.new("query lowering: an Allen-mask param is not a comparison term — masks live in allen()'s mask position");
408
- }
907
+ /** Lowers one idb atom: positional head bindings, `FieldId(i)` = head position i. */
908
+ function lowerIdbAtom(ctx, rec, vars, ids) {
909
+ const pred = ctx.recIds.get(rec);
910
+ if (pred === undefined) {
911
+ throw errors.new(`query lowering: rec ${rec.name} was declared by a different program`);
409
912
  }
410
- if (side.kind === "measure") {
411
- return { kind: "measure", var: ids.of(side.over) };
913
+ const arity = rec.rules[0]?.select.length;
914
+ if (arity !== undefined && vars.length !== arity) {
915
+ throw errors.new(`query lowering: idb ${rec.name} takes ${arity} positions, got ${vars.length}`);
412
916
  }
413
- if (sibling.kind === "term") {
414
- const value = sibling.value;
415
- if (value[term] === "maskParam") {
416
- throw errors.new("query lowering: an Allen-mask param cannot type a literal side");
917
+ const bindings = vars.map(function lowerPosition(name, position) {
918
+ return [position, { kind: "var", var: ids.of(name) }];
919
+ });
920
+ return { source: { kind: "idb", pred }, bindings };
921
+ }
922
+ /** Lowers one comparison side; literals tag by the sibling's anchor (op-aware at `pointIn`). */
923
+ function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
924
+ switch (side.kind) {
925
+ case "var":
926
+ return { kind: "var", var: ids.of(side.name) };
927
+ case "param":
928
+ return { kind: "param", param: paramIdOf(ctx, side.name) };
929
+ case "setParam":
930
+ return { kind: "paramSet", param: paramIdOf(ctx, side.name) };
931
+ case "measure":
932
+ return { kind: "measure", var: ids.of(side.name) };
933
+ case "literal": {
934
+ const anchor = cmpAnchorOf(ctx, rule, sibling);
935
+ if (anchor === undefined) {
936
+ throw errors.new("query lowering: a comparison literal needs a bound-variable, measure, or anchored-param sibling to type it");
937
+ }
938
+ return { kind: "literal", value: taggedCmpLiteral("comparison literal", anchor, side.value, op) };
417
939
  }
418
- return {
419
- kind: "literal",
420
- value: taggedCmpLiteral("comparison literal", value.data, side.value, op)
421
- };
940
+ }
941
+ }
942
+ /** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
943
+ function cmpAnchorOf(ctx, rule, sibling) {
944
+ if (sibling.kind === "var") {
945
+ return rule.varFields[sibling.name]?.field;
422
946
  }
423
947
  if (sibling.kind === "measure") {
424
- return {
425
- kind: "literal",
426
- value: taggedCmpLiteral("comparison literal", "measure", side.value, op)
427
- };
948
+ return "measure";
428
949
  }
429
- throw errors.new("query lowering: a comparison without a variable or parameter side is constant-valued");
950
+ if (sibling.kind === "param" || sibling.kind === "setParam") {
951
+ return ctx.params.get(sibling.name)?.anchor;
952
+ }
953
+ return undefined;
430
954
  }
431
955
  /** Lowers one comparison. */
432
- function lowerComparison(ctx, cmp, ids) {
433
- const op = cmp.op;
434
- if (op.kind === "allen") {
435
- const mask = op.mask.kind === "literal"
436
- ? { kind: "literal", mask: op.mask.mask }
437
- : { kind: "param", param: paramIdOf(ctx, op.mask.param, op.mask.param.name) };
956
+ function lowerComparison(ctx, rule, cmp, ids) {
957
+ if (cmp.op === "allen") {
958
+ const maskData = cmp.mask;
959
+ if (maskData === undefined) {
960
+ throw errors.new("query lowering: an allen comparison lost its mask");
961
+ }
962
+ const mask = maskData.kind === "literal"
963
+ ? { kind: "literal", mask: maskData.mask }
964
+ : { kind: "param", param: paramIdOf(ctx, maskData.name) };
438
965
  return {
439
966
  op: { kind: "allen", mask },
440
- lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, "allen"),
441
- rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, "allen")
967
+ lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, "allen"),
968
+ rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, "allen")
442
969
  };
443
970
  }
444
971
  return {
445
- op: { kind: op.kind },
446
- lhs: lowerCmpTerm(ctx, cmp.lhs, cmp.rhs, ids, op.kind),
447
- rhs: lowerCmpTerm(ctx, cmp.rhs, cmp.lhs, ids, op.kind)
972
+ op: { kind: cmp.op },
973
+ lhs: lowerCmpTerm(ctx, rule, cmp.lhs, cmp.rhs, ids, cmp.op),
974
+ rhs: lowerCmpTerm(ctx, rule, cmp.rhs, cmp.lhs, ids, cmp.op)
448
975
  };
449
976
  }
450
977
  /** Lowers one condition node (comparison leaf or and/or tree). */
451
- function lowerCondition(ctx, condition, ids) {
452
- if (condition.item === "cmp") {
453
- return { kind: "leaf", cmp: lowerComparison(ctx, condition, ids) };
978
+ function lowerCondition(ctx, rule, cond, ids) {
979
+ if (cond.kind === "cmp") {
980
+ return { kind: "leaf", cmp: lowerComparison(ctx, rule, cond, ids) };
454
981
  }
455
982
  return {
456
- kind: condition.op,
457
- children: condition.children.map(function lowerChild(child) {
458
- return lowerCondition(ctx, child, ids);
983
+ kind: cond.op,
984
+ children: cond.children.map(function lowerChild(child) {
985
+ return lowerCondition(ctx, rule, child, ids);
459
986
  })
460
987
  };
461
988
  }
@@ -467,40 +994,35 @@ function lowerFind(entry, ids) {
467
994
  if (entry.kind === "measure") {
468
995
  return { kind: "measure", var: ids.of(entry.over) };
469
996
  }
470
- const aggregate = entry.aggregate;
471
- switch (aggregate.op) {
997
+ const agg = entry.agg;
998
+ switch (agg.op) {
472
999
  case "count":
473
1000
  return { kind: "aggregate", op: { kind: "count" } };
474
1001
  case "countDistinct":
475
- return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(aggregate.over) };
1002
+ return { kind: "aggregate", op: { kind: "countDistinct" }, over: ids.of(agg.over) };
476
1003
  case "fold": {
477
- const over = aggregate.over;
478
- if (isTerm(over)) {
479
- return { kind: "aggregate", op: { kind: aggregate.fold }, over: ids.of(over) };
1004
+ if (typeof agg.over === "string") {
1005
+ return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) };
480
1006
  }
481
- return { kind: "aggregateMeasure", op: { kind: aggregate.fold }, over: ids.of(over.measure) };
1007
+ return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) };
482
1008
  }
483
1009
  case "arg":
484
- return {
485
- kind: "aggregate",
486
- op: { kind: aggregate.direction, key: ids.of(aggregate.key) },
487
- over: ids.of(aggregate.over)
488
- };
1010
+ return { kind: "aggregate", op: { kind: agg.direction, key: ids.of(agg.key) }, over: ids.of(agg.over) };
489
1011
  case "pack":
490
- return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(aggregate.over) };
1012
+ return { kind: "aggregate", op: { kind: "pack" }, over: ids.of(agg.over) };
491
1013
  }
492
1014
  }
493
1015
  /** One aggregate's var-free head-op kind (`AggOp::head_op`). */
494
- function headOpOf(aggregate) {
495
- switch (aggregate.op) {
1016
+ function headOpOf(agg) {
1017
+ switch (agg.op) {
496
1018
  case "count":
497
1019
  return "count";
498
1020
  case "countDistinct":
499
1021
  return "countDistinct";
500
1022
  case "fold":
501
- return aggregate.fold;
1023
+ return agg.fold;
502
1024
  case "arg":
503
- return aggregate.direction;
1025
+ return agg.direction;
504
1026
  case "pack":
505
1027
  return "pack";
506
1028
  }
@@ -511,42 +1033,51 @@ function headTermOf(column) {
511
1033
  if (entry.kind === "var" || entry.kind === "measure") {
512
1034
  return { kind: "var" };
513
1035
  }
514
- return { kind: "aggregate", op: headOpOf(entry.aggregate) };
1036
+ return { kind: "aggregate", op: headOpOf(entry.agg) };
515
1037
  }
516
- /** Lowers one rule: body walked in written order, finds supplied per shape. */
517
- function lowerRule(ctx, body, finds) {
518
- const ids = makeVarIds(ctx.registry);
1038
+ /** Lowers one rule: body walked in written order (var ids by first occurrence), finds last. */
1039
+ function lowerRule(ctx, rule) {
1040
+ const ids = makeVarIds();
519
1041
  const atoms = [];
520
1042
  const negated = [];
521
1043
  const conditions = [];
522
- const extraConditions = [];
523
- for (const item of body) {
524
- if (item.item === "atom") {
525
- const lowered = lowerAtom(ctx, item, ids, extraConditions);
526
- if (item.negated) {
527
- negated.push(lowered);
1044
+ for (const item of rule.items) {
1045
+ switch (item.kind) {
1046
+ case "atom": {
1047
+ atoms.push(lowerAtom(ctx, item.atom, ids));
1048
+ break;
528
1049
  }
529
- else {
530
- atoms.push(lowered);
1050
+ case "negated": {
1051
+ negated.push(lowerAtom(ctx, item.atom, ids));
1052
+ break;
1053
+ }
1054
+ case "idb": {
1055
+ atoms.push(lowerIdbAtom(ctx, item.rec, item.vars, ids));
1056
+ break;
1057
+ }
1058
+ case "cond": {
1059
+ conditions.push(lowerCondition(ctx, rule, item.cond, ids));
1060
+ break;
531
1061
  }
532
- continue;
533
1062
  }
534
- conditions.push(lowerCondition(ctx, item, ids));
535
1063
  }
536
1064
  return {
537
- finds: finds(ids),
1065
+ finds: rule.select.map(function findOf(column) {
1066
+ return lowerFind(column.entry, ids);
1067
+ }),
538
1068
  atoms,
539
1069
  negated,
540
- conditions: [...conditions, ...extraConditions]
1070
+ conditions
541
1071
  };
542
1072
  }
543
1073
  /**
544
1074
  * Lowers a query value to the bridge's `ProgramIr` — pure and stable: the
545
- * declared predicates in declaration order (`PredId` = index), the output
546
- * predicate (built from `rules` + `select`) appended last. Relations lower
547
- * by declaration ordinal, the law the engine's own manifest pins;
548
- * `db.prepare` re-verifies the alignment against the live manifest before
549
- * sending.
1075
+ * recs in declaration order (`PredId` = index), the output predicate
1076
+ * (rules + head) appended last. Relations lower by declaration ordinal,
1077
+ * the law the engine's own manifest pins; `db.prepare` re-verifies the
1078
+ * alignment against the live manifest before sending. Every registered
1079
+ * param must carry a field anchor by now — an unanchorable param (its
1080
+ * every use beside a literal) is refused here, naming it.
550
1081
  */
551
1082
  function lowerQuery(q) {
552
1083
  const theory = q.schema;
@@ -554,51 +1085,39 @@ function lowerQuery(q) {
554
1085
  Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
555
1086
  relationIds.set(name, index);
556
1087
  });
557
- const registry = q.data.registry;
558
- const predicateIds = new Map();
559
- registry.predicates.forEach(function assignPredId(pred, index) {
560
- predicateIds.set(pred, index);
1088
+ const recIds = new Map();
1089
+ q.data.recs.forEach(function assignPredId(rec, index) {
1090
+ recIds.set(rec, index);
1091
+ });
1092
+ const paramIds = new Map();
1093
+ const params = new Map();
1094
+ q.data.params.forEach(function assignParamId(entry, index) {
1095
+ if (entry.anchor === undefined && entry.shape !== "mask") {
1096
+ throw errors.new(`query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`);
1097
+ }
1098
+ paramIds.set(entry.name, index);
1099
+ params.set(entry.name, entry);
561
1100
  });
562
- const ctx = { theory, relationIds, predicateIds, registry, usedParams: new Set() };
563
- const predicates = registry.predicates.map(function lowerPredicate(pred) {
1101
+ const ctx = { theory, relationIds, recIds, paramIds, params };
1102
+ const predicates = q.data.recs.map(function lowerRec(rec) {
1103
+ const head = rec.rules[0];
1104
+ if (head === undefined) {
1105
+ throw errors.new(`query lowering: rec ${rec.name} has no rules`);
1106
+ }
564
1107
  return {
565
- head: pred.columns.map(function boundHead() {
566
- return { kind: "var" };
567
- }),
568
- rules: pred.rules.map(function lowerClause(rule) {
569
- return lowerRule(ctx, rule.body, function clauseFinds(ids) {
570
- return rule.finds.map(function findVar(variable) {
571
- return { kind: "var", var: ids.of(variable) };
572
- });
573
- });
1108
+ head: head.select.map(headTermOf),
1109
+ rules: rec.rules.map(function lowerRecRule(rule) {
1110
+ return lowerRule(ctx, rule);
574
1111
  })
575
1112
  };
576
1113
  });
577
1114
  predicates.push({
578
1115
  head: q.data.select.map(headTermOf),
579
- rules: q.data.rules.map(function lowerOutputRule(body) {
580
- return lowerRule(ctx, body, function outputFinds(ids) {
581
- return q.data.select.map(function findOf(column) {
582
- return lowerFind(column.entry, ids);
583
- });
584
- });
1116
+ rules: q.data.rules.map(function lowerOutputRule(rule) {
1117
+ return lowerRule(ctx, rule);
585
1118
  })
586
1119
  });
587
- /**
588
- * The dead-declaration refusal: a declared param no rule reached is
589
- * unexecutable BOTH ways — its inferred `Params` contribution is nothing
590
- * (params ride item phantoms), yet the wire marshal is the full registry
591
- * in declaration order, while the ENGINE's arity is usage-derived. The
592
- * contradiction is refused here, the earliest seam that knows usage, so
593
- * every query that lowers has registry == used set (and the dense-id
594
- * hole a skipped middle declaration would open never exists).
595
- */
596
- registry.params.forEach(function assertUsed(entry, index) {
597
- if (!ctx.usedParams.has(index)) {
598
- throw errors.new(`query declares param ${entry.name} but no rule uses it — remove the declaration or reference it`);
599
- }
600
- });
601
- return { predicates, output: registry.predicates.length };
1120
+ return { predicates, output: q.data.recs.length };
602
1121
  }
603
- export { lowerQuery, query, taggedLiteral };
1122
+ export { lowerQuery, makeOutputRuleScope, makeQuery, makeRawScope, query, taggedCmpLiteral, taggedLiteral };
604
1123
  //# sourceMappingURL=lower.js.map