@bjornpagen/bumbledb 0.2.0 → 0.4.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 (94) hide show
  1. package/COOKBOOK.md +503 -427
  2. package/README.md +84 -36
  3. package/dist/closed.d.ts +111 -38
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +94 -99
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +16 -2
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +46 -8
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +114 -69
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js +38 -21
  14. package/dist/face.js.map +1 -1
  15. package/dist/fields.d.ts +72 -87
  16. package/dist/fields.d.ts.map +1 -1
  17. package/dist/fields.js +35 -67
  18. package/dist/fields.js.map +1 -1
  19. package/dist/index.d.ts +18 -13
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +11 -7
  22. package/dist/index.js.map +1 -1
  23. package/dist/law.d.ts +224 -0
  24. package/dist/law.d.ts.map +1 -0
  25. package/dist/law.js +224 -0
  26. package/dist/law.js.map +1 -0
  27. package/dist/lower.d.ts +17 -10
  28. package/dist/lower.d.ts.map +1 -1
  29. package/dist/lower.js +34 -23
  30. package/dist/lower.js.map +1 -1
  31. package/dist/marshal.d.ts +33 -6
  32. package/dist/marshal.d.ts.map +1 -1
  33. package/dist/marshal.js +67 -6
  34. package/dist/marshal.js.map +1 -1
  35. package/dist/native.d.ts +6 -2
  36. package/dist/native.d.ts.map +1 -1
  37. package/dist/native.js.map +1 -1
  38. package/dist/query/atom.d.ts +139 -56
  39. package/dist/query/atom.d.ts.map +1 -1
  40. package/dist/query/atom.js +7 -1
  41. package/dist/query/atom.js.map +1 -1
  42. package/dist/query/lower.d.ts +71 -56
  43. package/dist/query/lower.d.ts.map +1 -1
  44. package/dist/query/lower.js +341 -69
  45. package/dist/query/lower.js.map +1 -1
  46. package/dist/query/predicate.d.ts +10 -9
  47. package/dist/query/predicate.d.ts.map +1 -1
  48. package/dist/query/predicate.js +2 -2
  49. package/dist/query/predicate.js.map +1 -1
  50. package/dist/query/run.d.ts +15 -5
  51. package/dist/query/run.d.ts.map +1 -1
  52. package/dist/query/run.js +26 -6
  53. package/dist/query/run.js.map +1 -1
  54. package/dist/query/scope.d.ts +100 -43
  55. package/dist/query/scope.d.ts.map +1 -1
  56. package/dist/query/scope.js +89 -30
  57. package/dist/query/scope.js.map +1 -1
  58. package/dist/query/select.d.ts +5 -5
  59. package/dist/query/select.d.ts.map +1 -1
  60. package/dist/relation.d.ts +29 -15
  61. package/dist/relation.d.ts.map +1 -1
  62. package/dist/relation.js +45 -17
  63. package/dist/relation.js.map +1 -1
  64. package/dist/schema.d.ts +41 -3
  65. package/dist/schema.d.ts.map +1 -1
  66. package/dist/schema.js +16 -2
  67. package/dist/schema.js.map +1 -1
  68. package/dist/spec.d.ts +10 -8
  69. package/dist/spec.d.ts.map +1 -1
  70. package/dist/spec.js.map +1 -1
  71. package/dist/statements.d.ts +67 -31
  72. package/dist/statements.d.ts.map +1 -1
  73. package/dist/statements.js +97 -20
  74. package/dist/statements.js.map +1 -1
  75. package/package.json +2 -2
  76. package/src/closed.ts +214 -146
  77. package/src/db.ts +65 -10
  78. package/src/face.ts +169 -102
  79. package/src/fields.ts +97 -164
  80. package/src/index.ts +43 -18
  81. package/src/law.ts +519 -0
  82. package/src/lower.ts +36 -23
  83. package/src/marshal.ts +74 -7
  84. package/src/native.ts +6 -2
  85. package/src/query/atom.ts +155 -65
  86. package/src/query/lower.ts +572 -167
  87. package/src/query/predicate.ts +43 -33
  88. package/src/query/run.ts +26 -6
  89. package/src/query/scope.ts +161 -51
  90. package/src/query/select.ts +5 -5
  91. package/src/relation.ts +60 -26
  92. package/src/schema.ts +48 -7
  93. package/src/spec.ts +10 -8
  94. package/src/statements.ts +165 -46
@@ -27,11 +27,12 @@
27
27
  import * as errors from "@superbuilders/errors";
28
28
  import { assertDeclarationOrderKey } from "#fields.ts";
29
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, renderFieldKind, term } from "#query/scope.ts";
30
+ import { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, makeVar, makeVars, renderFieldKind, term } from "#query/scope.ts";
31
31
  import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts";
32
32
  /** The frozen constructor vocabulary every rule builder spreads. */
33
33
  const termOps = Object.freeze({
34
34
  var: makeVar,
35
+ vars: makeVars,
35
36
  param: makeParam,
36
37
  inSet: makeSetParam,
37
38
  maskParam: makeMaskParam,
@@ -64,41 +65,113 @@ const EMPTY_RULE = Object.freeze({
64
65
  paramUses: Object.freeze([])
65
66
  });
66
67
  /**
67
- * Resolves a bindings record against a relation's declared fields, in the
68
- * record's written order: terms classify by their runtime tag, everything
69
- * else is a bare literal (typed by the FIELD at lowering — the membership
70
- * typing rule included).
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.
71
75
  */
72
- function resolveBindings(context, relation, bindings) {
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;
81
+ }
82
+ /**
83
+ * 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.
96
+ */
97
+ function membershipSet(context, field, value) {
98
+ if (!("closed" in field)) {
99
+ throw errors.new(`${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`);
100
+ }
101
+ if (value.length === 0) {
102
+ throw errors.new(`${context}: an empty membership array selects nothing — write the query you mean`);
103
+ }
104
+ if (value.length === 1) {
105
+ throw errors.new(`${context}: a one-element membership array is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`);
106
+ }
107
+ const seen = new Set();
108
+ const members = value.map(function memberName(member) {
109
+ if (typeof member !== "string") {
110
+ throw literalShapeError(context, `a ${field.closed.name} handle name (string)`, member);
111
+ }
112
+ if (seen.has(member)) {
113
+ throw errors.new(`${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`);
114
+ }
115
+ seen.add(member);
116
+ return member;
117
+ });
118
+ const key = [...members].sort();
119
+ return { name: `∈ ${field.closed.name} ${JSON.stringify(key)}`, members: Object.freeze(members) };
120
+ }
121
+ /**
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
124
+ * 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.
129
+ */
130
+ function resolveBindings(context, relation, bindings, classes) {
73
131
  const entries = [];
74
132
  const vars = [];
75
133
  const uses = [];
134
+ const relationClasses = classes[relation.name];
135
+ const ordered = matchFieldsOf(relation);
76
136
  for (const [fieldName, value] of Object.entries(bindings)) {
77
137
  if (value === undefined) {
78
138
  continue;
79
139
  }
80
- const declared = relation.data.fields.find(function byName(candidate) {
140
+ const declared = ordered.find(function byName(candidate) {
81
141
  return candidate.name === fieldName;
82
142
  });
83
143
  if (declared === undefined) {
84
144
  throw errors.new(`${context} has no field ${fieldName}`);
85
145
  }
146
+ const fieldClass = relationClasses?.[fieldName];
86
147
  let bound;
87
148
  if (isTerm(value)) {
88
149
  switch (value[term]) {
89
150
  case "var": {
90
151
  bound = Object.freeze({ kind: "var", name: value.name });
91
- vars.push(Object.freeze({ name: value.name, field: declared.field }));
152
+ vars.push(Object.freeze({ name: value.name, slot: Object.freeze({ field: declared.field, class: fieldClass }) }));
92
153
  break;
93
154
  }
94
155
  case "param": {
95
156
  bound = Object.freeze({ kind: "param", name: value.name });
96
- uses.push(Object.freeze({ name: value.name, shape: "value", anchor: declared.field, op: "binding" }));
157
+ uses.push(Object.freeze({
158
+ name: value.name,
159
+ shape: "value",
160
+ anchor: declared.field,
161
+ op: "binding",
162
+ members: undefined
163
+ }));
97
164
  break;
98
165
  }
99
166
  case "setParam": {
100
167
  bound = Object.freeze({ kind: "setParam", name: value.name });
101
- uses.push(Object.freeze({ name: value.name, shape: "set", anchor: declared.field, op: "binding" }));
168
+ uses.push(Object.freeze({
169
+ name: value.name,
170
+ shape: "set",
171
+ anchor: declared.field,
172
+ op: "binding",
173
+ members: undefined
174
+ }));
102
175
  break;
103
176
  }
104
177
  case "maskParam":
@@ -107,10 +180,21 @@ function resolveBindings(context, relation, bindings) {
107
180
  throw errors.new(`${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`);
108
181
  }
109
182
  }
183
+ else if (Array.isArray(value)) {
184
+ const set = membershipSet(`${context}.${fieldName}`, declared.field, value);
185
+ bound = Object.freeze({ kind: "literalSet", name: set.name, members: set.members });
186
+ uses.push(Object.freeze({
187
+ name: set.name,
188
+ shape: "set",
189
+ anchor: declared.field,
190
+ op: "binding",
191
+ members: set.members
192
+ }));
193
+ }
110
194
  else {
111
195
  bound = Object.freeze({ kind: "literal", value });
112
196
  }
113
- entries.push(Object.freeze({ field: fieldName, data: declared.field, term: bound }));
197
+ entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }));
114
198
  }
115
199
  return {
116
200
  atom: Object.freeze({ relation, bindings: Object.freeze(entries) }),
@@ -121,20 +205,20 @@ function resolveBindings(context, relation, bindings) {
121
205
  /**
122
206
  * Extends a rule state with one positive atom. Vars bind on first
123
207
  * occurrence; every LATER occurrence (a later atom's field or a same-record
124
- * sibling) is a join and must be domain-equal — the construction-time twin
125
- * of the type tier's `JoinOk`, so the domain wall holds for untyped
126
- * callers too.
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.
127
211
  */
128
- function advanceMatch(state, relation, bindings) {
129
- const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings);
212
+ function advanceMatch(state, relation, bindings, classes) {
213
+ const resolved = resolveBindings(`relation ${relation.name}`, relation, bindings, classes);
130
214
  const varFields = { ...state.varFields };
131
215
  for (const bound of resolved.vars) {
132
216
  const existing = varFields[bound.name];
133
217
  if (existing === undefined) {
134
- varFields[bound.name] = bound.field;
218
+ varFields[bound.name] = bound.slot;
135
219
  }
136
- else if (!fieldJoins(existing, bound.field)) {
137
- throw errors.new(`relation ${relation.name}: the variable ${bound.name} joins domain-unequal fields — first bound at ${renderFieldKind(existing)}, reused at ${renderFieldKind(bound.field)} (a var joins only domain-equal fields)`);
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)`);
138
222
  }
139
223
  }
140
224
  return {
@@ -173,7 +257,7 @@ function sideUses(op, side, sibling, varFields, uses) {
173
257
  }
174
258
  let anchor;
175
259
  if (sibling.kind === "var") {
176
- anchor = varFields[sibling.name];
260
+ anchor = varFields[sibling.name]?.field;
177
261
  }
178
262
  else if (sibling.kind === "measure") {
179
263
  anchor = "measure";
@@ -185,7 +269,8 @@ function sideUses(op, side, sibling, varFields, uses) {
185
269
  name: side.name,
186
270
  shape: side.kind === "param" ? "value" : "set",
187
271
  anchor,
188
- op
272
+ op,
273
+ members: undefined
189
274
  }));
190
275
  }
191
276
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
@@ -203,7 +288,13 @@ function condDataOf(cond, varFields, uses) {
203
288
  }
204
289
  else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
205
290
  mask = Object.freeze({ kind: "param", name: maskValue.name });
206
- uses.push(Object.freeze({ name: maskValue.name, shape: "mask", anchor: undefined, op: "allen" }));
291
+ uses.push(Object.freeze({
292
+ name: maskValue.name,
293
+ shape: "mask",
294
+ anchor: undefined,
295
+ op: "allen",
296
+ members: undefined
297
+ }));
207
298
  }
208
299
  else {
209
300
  throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam");
@@ -222,7 +313,7 @@ function condDataOf(cond, varFields, uses) {
222
313
  throw errors.new("a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()");
223
314
  }
224
315
  /** Extends a rule state with one `.where` item (a condition or a negated atom). */
225
- function advanceWhere(state, cond) {
316
+ function advanceWhere(state, cond, classes) {
226
317
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
227
318
  throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom");
228
319
  }
@@ -231,7 +322,7 @@ function advanceWhere(state, cond) {
231
322
  const bindings = Object.fromEntries(Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
232
323
  return value !== undefined;
233
324
  }));
234
- const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings);
325
+ const resolved = resolveBindings(`negated relation ${relation.name}`, relation, bindings, classes);
235
326
  return {
236
327
  items: Object.freeze([...state.items, Object.freeze({ kind: "negated", atom: resolved.atom })]),
237
328
  varFields: state.varFields,
@@ -264,14 +355,27 @@ function advanceIdb(state, rec, vars) {
264
355
  function isAggregateEntry(value) {
265
356
  return typeof value === "object" && value !== null && "agg" in value;
266
357
  }
267
- /** Classifies one select entry into its named answer column. */
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
+ */
268
364
  function selectColumnOf(entry) {
269
365
  if (typeof entry === "string") {
270
- return Object.freeze({ name: entry, entry: Object.freeze({ kind: "var", over: entry }) });
366
+ return Object.freeze({
367
+ name: entry,
368
+ entry: Object.freeze({ kind: "var", over: entry }),
369
+ closed: undefined
370
+ });
271
371
  }
272
372
  if (isTerm(entry)) {
273
373
  if (entry[term] === "duration") {
274
- return Object.freeze({ name: entry.name, entry: Object.freeze({ kind: "measure", over: entry.name }) });
374
+ return Object.freeze({
375
+ name: entry.name,
376
+ entry: Object.freeze({ kind: "measure", over: entry.name }),
377
+ closed: undefined
378
+ });
275
379
  }
276
380
  throw errors.new(`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`);
277
381
  }
@@ -283,7 +387,11 @@ function selectColumnOf(entry) {
283
387
  /** Classifies one aggregate select entry. */
284
388
  function aggregateColumnOf(entry) {
285
389
  function column(name, agg) {
286
- return Object.freeze({ name, entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }) });
390
+ return Object.freeze({
391
+ name,
392
+ entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }),
393
+ closed: undefined
394
+ });
287
395
  }
288
396
  const over = entry.over;
289
397
  switch (entry.agg) {
@@ -323,32 +431,66 @@ function aggregateColumnOf(entry) {
323
431
  throw errors.new(`unknown aggregate ${entry.agg}`);
324
432
  }
325
433
  }
434
+ /**
435
+ * 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).
442
+ */
443
+ function closedOrderError(context, position, vocabulary) {
444
+ 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
+ }
446
+ /** The comparison ops the orderable ban covers (order roster + point membership — every order-comparison position). */
447
+ function isOrderOp(op) {
448
+ return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn";
449
+ }
326
450
  /** Requires a var name to be bound by a relation atom of the rule. */
327
451
  function assertBound(context, varFields, name) {
328
- const field = varFields[name];
329
- if (field === undefined) {
452
+ const slot = varFields[name];
453
+ if (slot === undefined) {
330
454
  throw errors.new(`${context}: the variable ${name} is not bound by a relation atom of the rule`);
331
455
  }
332
- return field;
456
+ return slot;
333
457
  }
334
458
  /** Requires a var name to be bound at an interval field (the measure's and pack's domain). */
335
459
  function assertIntervalBound(context, varFields, name) {
336
- const field = assertBound(context, varFields, name);
337
- if (field.kind !== "interval") {
460
+ const slot = assertBound(context, varFields, name);
461
+ if (slot.field.kind !== "interval") {
338
462
  throw errors.new(`${context}: ${name} is not interval-typed — the measure is defined over interval-typed variables only`);
339
463
  }
340
464
  }
341
- /** Validates one condition's variable references against the rule's bound names. */
465
+ /**
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.
473
+ */
342
474
  function validateCond(context, varFields, cond) {
343
475
  if (cond.kind === "cmp") {
344
476
  for (const side of [cond.lhs, cond.rhs]) {
345
477
  if (side.kind === "var") {
346
- assertBound(context, varFields, side.name);
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
+ }
347
482
  }
348
483
  if (side.kind === "measure") {
349
484
  assertIntervalBound(context, varFields, side.name);
350
485
  }
351
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
+ }
352
494
  return;
353
495
  }
354
496
  for (const child of cond.children) {
@@ -375,21 +517,58 @@ function validateColumn(context, varFields, column) {
375
517
  return;
376
518
  case "fold": {
377
519
  if (typeof agg.over === "string") {
378
- assertBound(`${context} select ${column.name}`, varFields, agg.over);
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
+ }
379
524
  return;
380
525
  }
381
526
  assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration);
382
527
  return;
383
528
  }
384
- case "arg":
529
+ case "arg": {
385
530
  assertBound(`${context} select ${column.name}`, varFields, agg.over);
386
- assertBound(`${context} select ${column.name}`, varFields, agg.key);
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
+ }
387
535
  return;
536
+ }
388
537
  case "pack":
389
538
  assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over);
390
539
  return;
391
540
  }
392
541
  }
542
+ /**
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.
551
+ */
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;
565
+ }
566
+ const field = varFields[over]?.field;
567
+ if (field !== undefined && "closed" in field) {
568
+ return field.closed;
569
+ }
570
+ return undefined;
571
+ }
393
572
  /**
394
573
  * Completes one rule: classifies the select record (written order = answer
395
574
  * column order, names must be declaration-order-safe keys), and validates
@@ -418,8 +597,9 @@ function completeRule(context, state, columns) {
418
597
  if (bound === undefined) {
419
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)`);
420
599
  }
421
- if (!fieldJoins(bound, binding.data)) {
422
- throw errors.new(`${context}: negated ${item.atom.relation.name} atom reuses the variable ${binding.term.name} at ${binding.field} (${renderFieldKind(binding.data)}), but the rule binds it at ${renderFieldKind(bound)} — a var joins only domain-equal fields`);
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`);
423
603
  }
424
604
  }
425
605
  }
@@ -435,9 +615,9 @@ function completeRule(context, state, columns) {
435
615
  if (column === undefined || column.entry.kind !== "var") {
436
616
  return;
437
617
  }
438
- const headField = head?.varFields[column.entry.over];
439
- if (headField !== undefined && !fieldJoins(headField, bound)) {
440
- throw errors.new(`${context}: idb ${item.rec.name} joins the variable ${name} (${renderFieldKind(bound)}) at head position ${position} (${column.name}: ${renderFieldKind(headField)}) — a var joins only domain-equal fields`);
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`);
441
621
  }
442
622
  });
443
623
  }
@@ -447,7 +627,13 @@ function completeRule(context, state, columns) {
447
627
  }
448
628
  return Object.freeze({
449
629
  items: state.items,
450
- select: Object.freeze([...columns]),
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
+ })),
451
637
  varFields: state.varFields,
452
638
  paramUses: state.paramUses
453
639
  });
@@ -496,10 +682,10 @@ function selectColumns(context, entries) {
496
682
  function makeRawChain(context, state) {
497
683
  const chain = {
498
684
  match(relation, bindings) {
499
- return makeRawChain(context, advanceMatch(state, relation, bindings));
685
+ return makeRawChain(context, advanceMatch(state, relation, bindings, context.classes));
500
686
  },
501
687
  where(cond) {
502
- return makeRawChain(context, advanceWhere(state, cond));
688
+ return makeRawChain(context, advanceWhere(state, cond, context.classes));
503
689
  },
504
690
  idb(target, ...vars) {
505
691
  return makeRawChain(context, idbAdvance(context, state, target, vars));
@@ -516,7 +702,7 @@ function makeRawScope(context) {
516
702
  const scope = {
517
703
  ...termOps,
518
704
  match(relation, bindings) {
519
- return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings));
705
+ return makeRawChain(context, advanceMatch(EMPTY_RULE, relation, bindings, context.classes));
520
706
  }
521
707
  };
522
708
  Object.freeze(scope);
@@ -535,8 +721,8 @@ function isTypedScope(scope) {
535
721
  return typeof scope.match === "function";
536
722
  }
537
723
  /** Builds one query-rule builder (the typed face of the raw builder). */
538
- function makeQueryRuleScope() {
539
- const raw = makeRawScope({ kind: "query" });
724
+ function makeQueryRuleScope(classes) {
725
+ const raw = makeRawScope({ kind: "query", classes });
540
726
  if (!isTypedScope(raw)) {
541
727
  throw errors.new("query rule builder construction incomplete");
542
728
  }
@@ -544,12 +730,16 @@ function makeQueryRuleScope() {
544
730
  }
545
731
  /** Builds one output-rule builder over a program's recs. */
546
732
  function makeOutputRuleScope(program) {
547
- const raw = makeRawScope({ kind: "output", program });
733
+ const raw = makeRawScope({ kind: "output", program, classes: program.classes });
548
734
  if (!isTypedScope(raw)) {
549
735
  throw errors.new("program output rule builder construction incomplete");
550
736
  }
551
737
  return raw;
552
738
  }
739
+ /** Renders one head column's closed slice for the rule-alignment check's diagnostics. */
740
+ function renderClosedSlice(closed) {
741
+ return closed === undefined ? "a bare value" : `a ${closed.name} reference`;
742
+ }
553
743
  /** Renders one head column's signature for the rule-alignment check. */
554
744
  function headSignature(column) {
555
745
  const entry = column.entry;
@@ -565,11 +755,36 @@ function headSignature(column) {
565
755
  }
566
756
  return `${column.name}:${agg.op}`;
567
757
  }
758
+ /** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
759
+ 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;
767
+ }
768
+ /** Renders one param anchor's closedness for the registry's coherence diagnostics. */
769
+ function renderParamAnchor(roster) {
770
+ return roster === undefined ? "a non-closed position" : `a ${roster.name} reference`;
771
+ }
568
772
  /**
569
773
  * Folds every rule's param uses (recs in declaration order first, output
570
774
  * rules last — exactly the lowering walk) into the query's registry:
571
775
  * first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
572
- * the wire, and one name must keep one shape.
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).
573
788
  */
574
789
  function paramRegistryOf(recs, rules) {
575
790
  const order = [];
@@ -579,16 +794,35 @@ function paramRegistryOf(recs, rules) {
579
794
  const existing = byName.get(use.name);
580
795
  if (existing === undefined) {
581
796
  order.push(use.name);
582
- byName.set(use.name, { shape: use.shape, anchor: use.anchor, op: use.op });
797
+ byName.set(use.name, {
798
+ shape: use.shape,
799
+ anchor: use.anchor,
800
+ op: use.op,
801
+ members: use.members,
802
+ orderOp: isOrderOp(use.op) ? use.op : undefined
803
+ });
583
804
  continue;
584
805
  }
806
+ if ((existing.members === undefined) !== (use.members === undefined)) {
807
+ throw errors.new(`query param ${use.name} collides with a membership array's registry entry — name the param differently`);
808
+ }
585
809
  if (existing.shape !== use.shape) {
586
810
  throw errors.new(`query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`);
587
811
  }
812
+ if (existing.anchor !== undefined && use.anchor !== undefined) {
813
+ const registered = anchorRosterOf(existing.anchor);
814
+ const anchored = anchorRosterOf(use.anchor);
815
+ if (registered !== anchored) {
816
+ throw errors.new(`query param ${use.name} is anchored at ${renderParamAnchor(registered)} and at ${renderParamAnchor(anchored)} — a closed-anchored param translates handle names through ONE roster (one name, one domain); name the params differently`);
817
+ }
818
+ }
588
819
  if (existing.anchor === undefined && use.anchor !== undefined) {
589
820
  existing.anchor = use.anchor;
590
821
  existing.op = use.op;
591
822
  }
823
+ if (existing.orderOp === undefined && isOrderOp(use.op)) {
824
+ existing.orderOp = use.op;
825
+ }
592
826
  }
593
827
  }
594
828
  for (const rec of recs) {
@@ -604,7 +838,13 @@ function paramRegistryOf(recs, rules) {
604
838
  if (entry === undefined) {
605
839
  throw errors.new(`query param ${name} lost its registry entry`);
606
840
  }
607
- return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op });
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);
846
+ }
847
+ return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members });
608
848
  }));
609
849
  }
610
850
  /**
@@ -624,6 +864,17 @@ function makeRawQuery(theory, recs, rules) {
624
864
  if (candidate !== signature) {
625
865
  throw errors.new(`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`);
626
866
  }
867
+ // 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];
874
+ if (lead !== undefined && column.closed !== lead.closed) {
875
+ 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
+ }
877
+ });
627
878
  });
628
879
  const data = Object.freeze({
629
880
  recs: Object.freeze([...recs]),
@@ -635,7 +886,7 @@ function makeRawQuery(theory, recs, rules) {
635
886
  schema: theory,
636
887
  data,
637
888
  rule(build) {
638
- const built = build(makeRawScope({ kind: "query" }));
889
+ const built = build(makeRawScope({ kind: "query", classes: theory.classes }));
639
890
  return makeRawQuery(theory, recs, [...rules, built.rule]);
640
891
  }
641
892
  };
@@ -661,12 +912,14 @@ function makeQuery(theory, recs, rules) {
661
912
  /**
662
913
  * Opens a query over a schema: `query(S).rule(r => ...)`. Each `.rule`
663
914
  * adds one conjunctive rule; multiple rules are the set union (answers are
664
- * SETS — no order or limit exists anywhere; hosts sort).
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.
665
918
  */
666
919
  function query(theory) {
667
920
  const start = {
668
921
  rule(build) {
669
- const built = build(makeQueryRuleScope());
922
+ const built = build(makeQueryRuleScope(theory.classes));
670
923
  return makeQuery(theory, [], [built.rule]);
671
924
  }
672
925
  };
@@ -687,19 +940,24 @@ function isIntervalShaped(value) {
687
940
  typeof value.end === "bigint");
688
941
  }
689
942
  /**
690
- * Tags one closed-reference literal: the bare handle id, verified against
691
- * the roster (the belt the type level cannot provide — structural values
692
- * make any bigint spellable here) and tagged u64 — queries cross ids,
693
- * never handle names.
943
+ * 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).
694
951
  */
695
952
  function taggedHandleId(context, closed, value) {
696
- if (typeof value !== "bigint") {
697
- throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value);
953
+ if (typeof value !== "string") {
954
+ throw literalShapeError(context, `a ${closed.name} handle name (string)`, value);
698
955
  }
699
- if (closed.handles[Number(value)] === undefined) {
700
- throw errors.new(`${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`);
956
+ const id = closed.handles.indexOf(value);
957
+ if (id < 0) {
958
+ throw errors.new(`${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`);
701
959
  }
702
- return { kind: "u64", value };
960
+ return { kind: "u64", value: BigInt(id) };
703
961
  }
704
962
  /**
705
963
  * Tags one literal in an interval element domain: a bigint tags as the
@@ -822,7 +1080,13 @@ function paramIdOf(ctx, name) {
822
1080
  }
823
1081
  return id;
824
1082
  }
825
- /** Lowers one EDB atom (either polarity). */
1083
+ /**
1084
+ * 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).
1089
+ */
826
1090
  function lowerAtom(ctx, atom, ids) {
827
1091
  const member = ctx.theory.relations[atom.relation.name];
828
1092
  if (member !== atom.relation) {
@@ -832,8 +1096,9 @@ function lowerAtom(ctx, atom, ids) {
832
1096
  if (relationId === undefined) {
833
1097
  throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`);
834
1098
  }
1099
+ const ordered = matchFieldsOf(atom.relation);
835
1100
  const bindings = atom.bindings.map(function lowerBinding(binding) {
836
- const ordinal = atom.relation.data.fields.findIndex(function byName(candidate) {
1101
+ const ordinal = ordered.findIndex(function byName(candidate) {
837
1102
  return candidate.name === binding.field;
838
1103
  });
839
1104
  if (ordinal < 0) {
@@ -843,7 +1108,12 @@ function lowerAtom(ctx, atom, ids) {
843
1108
  });
844
1109
  return { source: { kind: "edb", relation: relationId }, bindings };
845
1110
  }
846
- /** Lowers one binding term. */
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
+ */
847
1117
  function lowerBindingTerm(ctx, context, binding, ids) {
848
1118
  const bound = binding.term;
849
1119
  switch (bound.kind) {
@@ -853,6 +1123,8 @@ function lowerBindingTerm(ctx, context, binding, ids) {
853
1123
  return { kind: "param", param: paramIdOf(ctx, bound.name) };
854
1124
  case "setParam":
855
1125
  return { kind: "paramSet", param: paramIdOf(ctx, bound.name) };
1126
+ case "literalSet":
1127
+ return { kind: "paramSet", param: paramIdOf(ctx, bound.name) };
856
1128
  case "literal":
857
1129
  return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
858
1130
  }
@@ -895,7 +1167,7 @@ function lowerCmpTerm(ctx, rule, side, sibling, ids, op) {
895
1167
  /** Resolves the anchor a comparison literal tags by: the sibling's field, the measure, or an anchored param. */
896
1168
  function cmpAnchorOf(ctx, rule, sibling) {
897
1169
  if (sibling.kind === "var") {
898
- return rule.varFields[sibling.name];
1170
+ return rule.varFields[sibling.name]?.field;
899
1171
  }
900
1172
  if (sibling.kind === "measure") {
901
1173
  return "measure";