@bjornpagen/bumbledb 0.3.0 → 0.5.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 (98) hide show
  1. package/COOKBOOK.md +246 -92
  2. package/README.md +31 -16
  3. package/dist/closed.d.ts +80 -75
  4. package/dist/closed.d.ts.map +1 -1
  5. package/dist/closed.js +102 -127
  6. package/dist/closed.js.map +1 -1
  7. package/dist/db.d.ts +37 -7
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +95 -55
  10. package/dist/db.js.map +1 -1
  11. package/dist/exhume.d.ts.map +1 -1
  12. package/dist/exhume.js +1 -14
  13. package/dist/exhume.js.map +1 -1
  14. package/dist/face.d.ts +40 -40
  15. package/dist/face.d.ts.map +1 -1
  16. package/dist/face.js +9 -17
  17. package/dist/face.js.map +1 -1
  18. package/dist/fields.d.ts +46 -15
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +58 -29
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +13 -8
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/law.d.ts +2 -1
  27. package/dist/law.d.ts.map +1 -1
  28. package/dist/law.js +15 -14
  29. package/dist/law.js.map +1 -1
  30. package/dist/lower.d.ts.map +1 -1
  31. package/dist/lower.js +1 -7
  32. package/dist/lower.js.map +1 -1
  33. package/dist/marshal.d.ts +33 -6
  34. package/dist/marshal.d.ts.map +1 -1
  35. package/dist/marshal.js +75 -26
  36. package/dist/marshal.js.map +1 -1
  37. package/dist/native.d.ts +21 -2
  38. package/dist/native.d.ts.map +1 -1
  39. package/dist/native.js +20 -3
  40. package/dist/native.js.map +1 -1
  41. package/dist/order.d.ts +36 -0
  42. package/dist/order.d.ts.map +1 -0
  43. package/dist/order.js +135 -0
  44. package/dist/order.js.map +1 -0
  45. package/dist/query/atom.d.ts +76 -28
  46. package/dist/query/atom.d.ts.map +1 -1
  47. package/dist/query/atom.js +12 -16
  48. package/dist/query/atom.js.map +1 -1
  49. package/dist/query/lower.d.ts +5 -8
  50. package/dist/query/lower.d.ts.map +1 -1
  51. package/dist/query/lower.js +308 -72
  52. package/dist/query/lower.js.map +1 -1
  53. package/dist/query/predicate.d.ts.map +1 -1
  54. package/dist/query/predicate.js +34 -2
  55. package/dist/query/predicate.js.map +1 -1
  56. package/dist/query/run.d.ts +15 -5
  57. package/dist/query/run.d.ts.map +1 -1
  58. package/dist/query/run.js +27 -8
  59. package/dist/query/run.js.map +1 -1
  60. package/dist/query/scope.d.ts +37 -29
  61. package/dist/query/scope.d.ts.map +1 -1
  62. package/dist/query/scope.js +19 -47
  63. package/dist/query/scope.js.map +1 -1
  64. package/dist/relation.d.ts +17 -29
  65. package/dist/relation.d.ts.map +1 -1
  66. package/dist/relation.js +41 -38
  67. package/dist/relation.js.map +1 -1
  68. package/dist/schema.d.ts.map +1 -1
  69. package/dist/schema.js +7 -31
  70. package/dist/schema.js.map +1 -1
  71. package/dist/spec.d.ts +3 -2
  72. package/dist/spec.d.ts.map +1 -1
  73. package/dist/spec.js.map +1 -1
  74. package/dist/statements.d.ts +13 -4
  75. package/dist/statements.d.ts.map +1 -1
  76. package/dist/statements.js +75 -8
  77. package/dist/statements.js.map +1 -1
  78. package/package.json +2 -5
  79. package/src/closed.ts +144 -206
  80. package/src/db.ts +143 -68
  81. package/src/exhume.ts +1 -15
  82. package/src/face.ts +38 -48
  83. package/src/fields.ts +103 -49
  84. package/src/index.ts +11 -10
  85. package/src/law.ts +15 -14
  86. package/src/lower.ts +2 -9
  87. package/src/marshal.ts +82 -31
  88. package/src/native.ts +22 -4
  89. package/src/order.ts +156 -0
  90. package/src/query/atom.ts +70 -35
  91. package/src/query/lower.ts +354 -82
  92. package/src/query/predicate.ts +39 -4
  93. package/src/query/run.ts +27 -9
  94. package/src/query/scope.ts +52 -70
  95. package/src/relation.ts +54 -68
  96. package/src/schema.ts +7 -33
  97. package/src/spec.ts +3 -2
  98. package/src/statements.ts +82 -8
@@ -25,14 +25,14 @@
25
25
  * pre-checked here.
26
26
  */
27
27
  import * as errors from "@superbuilders/errors";
28
- import { assertDeclarationOrderKey } from "#fields.ts";
29
- import { allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts";
30
- import { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, makeVar, makeVars, renderFieldKind, term } from "#query/scope.ts";
28
+ import { sealedFieldsOf } from "#closed.ts";
29
+ import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts";
30
+ import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts";
31
+ import { fieldJoins, inferred, isTerm, makeDuration, makeMaskParam, makeParam, makeSetParam, makeVar, renderFieldKind, term } from "#query/scope.ts";
31
32
  import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts";
32
33
  /** The frozen constructor vocabulary every rule builder spreads. */
33
34
  const termOps = Object.freeze({
34
35
  var: makeVar,
35
- vars: makeVars,
36
36
  param: makeParam,
37
37
  inSet: makeSetParam,
38
38
  maskParam: makeMaskParam,
@@ -44,7 +44,6 @@ const termOps = Object.freeze({
44
44
  gt,
45
45
  ge,
46
46
  pointIn,
47
- covers,
48
47
  allen,
49
48
  and,
50
49
  or,
@@ -65,19 +64,44 @@ const EMPTY_RULE = Object.freeze({
65
64
  paramUses: Object.freeze([])
66
65
  });
67
66
  /**
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.
67
+ * Judges one membership ARRAY at a binding positionlegal exactly at a
68
+ * CLOSED-reference field (the owner ruling: ordinary u64/str membership is
69
+ * spelled through `r.inSet` params; literal arrays are the closed
70
+ * vocabulary's spelling), holding 2 DISTINCT handle names (the
71
+ * degenerate sets are refusals: empty selects nothing, one element is the
72
+ * bare literal respelled, and a duplicate member is the same respelling in
73
+ * disguise — write each member once). The returned name is
74
+ * CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
75
+ * so two spellings of one set, reordered or not, share one dense
76
+ * `ParamId`); the members are shape-checked strings here and
77
+ * roster-verified at the one verification point (`taggedHandleId`) when
78
+ * the SDK supplies the set at execute — the same moment a bound `r.inSet`
79
+ * param's members are judged.
75
80
  */
76
- function matchFieldsOf(owner) {
77
- if ("axioms" in owner) {
78
- return [Object.freeze({ name: "id", field: owner.id }), ...owner.data.columns];
81
+ function membershipSet(context, field, value) {
82
+ const roster = rosterOf(field);
83
+ if (roster === undefined) {
84
+ throw errors.new(`${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`);
79
85
  }
80
- return owner.data.fields;
86
+ if (value.length === 0) {
87
+ throw errors.new(`${context}: an empty membership array selects nothing — write the query you mean`);
88
+ }
89
+ if (value.length === 1) {
90
+ 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)`);
91
+ }
92
+ const seen = new Set();
93
+ const members = value.map(function memberName(member) {
94
+ if (typeof member !== "string") {
95
+ throw literalShapeError(context, `a ${roster.name} handle name (string)`, member);
96
+ }
97
+ if (seen.has(member)) {
98
+ throw errors.new(`${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`);
99
+ }
100
+ seen.add(member);
101
+ return member;
102
+ });
103
+ const key = [...members].sort();
104
+ return { name: `∈ ${roster.name} ${JSON.stringify(key)}`, members: Object.freeze(members) };
81
105
  }
82
106
  /**
83
107
  * Resolves a bindings record against an atom owner's matchable fields (a
@@ -93,7 +117,7 @@ function resolveBindings(context, relation, bindings, classes) {
93
117
  const vars = [];
94
118
  const uses = [];
95
119
  const relationClasses = classes[relation.name];
96
- const ordered = matchFieldsOf(relation);
120
+ const ordered = sealedFieldsOf(relation);
97
121
  for (const [fieldName, value] of Object.entries(bindings)) {
98
122
  if (value === undefined) {
99
123
  continue;
@@ -115,12 +139,24 @@ function resolveBindings(context, relation, bindings, classes) {
115
139
  }
116
140
  case "param": {
117
141
  bound = Object.freeze({ kind: "param", name: value.name });
118
- uses.push(Object.freeze({ name: value.name, shape: "value", anchor: declared.field, op: "binding" }));
142
+ uses.push(Object.freeze({
143
+ name: value.name,
144
+ shape: "value",
145
+ anchor: declared.field,
146
+ op: "binding",
147
+ members: undefined
148
+ }));
119
149
  break;
120
150
  }
121
151
  case "setParam": {
122
152
  bound = Object.freeze({ kind: "setParam", name: value.name });
123
- uses.push(Object.freeze({ name: value.name, shape: "set", anchor: declared.field, op: "binding" }));
153
+ uses.push(Object.freeze({
154
+ name: value.name,
155
+ shape: "set",
156
+ anchor: declared.field,
157
+ op: "binding",
158
+ members: undefined
159
+ }));
124
160
  break;
125
161
  }
126
162
  case "maskParam":
@@ -129,6 +165,17 @@ function resolveBindings(context, relation, bindings, classes) {
129
165
  throw errors.new(`${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`);
130
166
  }
131
167
  }
168
+ else if (Array.isArray(value)) {
169
+ const set = membershipSet(`${context}.${fieldName}`, declared.field, value);
170
+ bound = Object.freeze({ kind: "literalSet", name: set.name, members: set.members });
171
+ uses.push(Object.freeze({
172
+ name: set.name,
173
+ shape: "set",
174
+ anchor: declared.field,
175
+ op: "binding",
176
+ members: set.members
177
+ }));
178
+ }
132
179
  else {
133
180
  bound = Object.freeze({ kind: "literal", value });
134
181
  }
@@ -207,7 +254,8 @@ function sideUses(op, side, sibling, varFields, uses) {
207
254
  name: side.name,
208
255
  shape: side.kind === "param" ? "value" : "set",
209
256
  anchor,
210
- op
257
+ op,
258
+ members: undefined
211
259
  }));
212
260
  }
213
261
  /** Lowers one condition VALUE to its runtime data, recording param uses. */
@@ -225,7 +273,13 @@ function condDataOf(cond, varFields, uses) {
225
273
  }
226
274
  else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
227
275
  mask = Object.freeze({ kind: "param", name: maskValue.name });
228
- uses.push(Object.freeze({ name: maskValue.name, shape: "mask", anchor: undefined, op: "allen" }));
276
+ uses.push(Object.freeze({
277
+ name: maskValue.name,
278
+ shape: "mask",
279
+ anchor: undefined,
280
+ op: "allen",
281
+ members: undefined
282
+ }));
229
283
  }
230
284
  else {
231
285
  throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam");
@@ -286,14 +340,27 @@ function advanceIdb(state, rec, vars) {
286
340
  function isAggregateEntry(value) {
287
341
  return typeof value === "object" && value !== null && "agg" in value;
288
342
  }
289
- /** Classifies one select entry into its named answer column. */
343
+ /**
344
+ * Classifies one select entry into its named answer column. The `closed`
345
+ * slice is resolved LATER, at rule completion (`completeRule`), where the
346
+ * rule's `varFields` are in hand — until then every column is provisionally
347
+ * bare.
348
+ */
290
349
  function selectColumnOf(entry) {
291
350
  if (typeof entry === "string") {
292
- return Object.freeze({ name: entry, entry: Object.freeze({ kind: "var", over: entry }) });
351
+ return Object.freeze({
352
+ name: entry,
353
+ entry: Object.freeze({ kind: "var", over: entry }),
354
+ closed: undefined
355
+ });
293
356
  }
294
357
  if (isTerm(entry)) {
295
358
  if (entry[term] === "duration") {
296
- return Object.freeze({ name: entry.name, entry: Object.freeze({ kind: "measure", over: entry.name }) });
359
+ return Object.freeze({
360
+ name: entry.name,
361
+ entry: Object.freeze({ kind: "measure", over: entry.name }),
362
+ closed: undefined
363
+ });
297
364
  }
298
365
  throw errors.new(`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`);
299
366
  }
@@ -305,7 +372,11 @@ function selectColumnOf(entry) {
305
372
  /** Classifies one aggregate select entry. */
306
373
  function aggregateColumnOf(entry) {
307
374
  function column(name, agg) {
308
- return Object.freeze({ name, entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }) });
375
+ return Object.freeze({
376
+ name,
377
+ entry: Object.freeze({ kind: "aggregate", agg: Object.freeze(agg) }),
378
+ closed: undefined
379
+ });
309
380
  }
310
381
  const over = entry.over;
311
382
  switch (entry.agg) {
@@ -345,6 +416,22 @@ function aggregateColumnOf(entry) {
345
416
  throw errors.new(`unknown aggregate ${entry.agg}`);
346
417
  }
347
418
  }
419
+ /**
420
+ * The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
421
+ * § orderability): a closed reference is equality-and-membership only —
422
+ * its declaration-id order is an encoding accident, so every
423
+ * order-comparison and fold position refuses it. The construction-time
424
+ * twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
425
+ * untyped callers too (the engine cannot backstop this one: the wire IR
426
+ * carries plain u64s, no rosters).
427
+ */
428
+ function closedOrderError(context, position, vocabulary) {
429
+ return errors.new(`${context}: ${position} is a ${vocabulary} reference — declaration order is an accident, not semantics: vocabularies do not order (docs/architecture/10-data-model.md; equality, membership, and counting remain)`);
430
+ }
431
+ /** The comparison ops under the orderable ban (order roster + point membership — every order-comparison position). */
432
+ function isOrderOp(op) {
433
+ return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn";
434
+ }
348
435
  /** Requires a var name to be bound by a relation atom of the rule. */
349
436
  function assertBound(context, varFields, name) {
350
437
  const slot = varFields[name];
@@ -373,7 +460,11 @@ function validateCond(context, varFields, cond) {
373
460
  if (cond.kind === "cmp") {
374
461
  for (const side of [cond.lhs, cond.rhs]) {
375
462
  if (side.kind === "var") {
376
- assertBound(context, varFields, side.name);
463
+ const slot = assertBound(context, varFields, side.name);
464
+ const roster = rosterOf(slot.field);
465
+ if (isOrderOp(cond.op) && roster !== undefined) {
466
+ throw closedOrderError(context, `the ${cond.op} side ${side.name}`, roster.name);
467
+ }
377
468
  }
378
469
  if (side.kind === "measure") {
379
470
  assertIntervalBound(context, varFields, side.name);
@@ -412,21 +503,56 @@ function validateColumn(context, varFields, column) {
412
503
  return;
413
504
  case "fold": {
414
505
  if (typeof agg.over === "string") {
415
- assertBound(`${context} select ${column.name}`, varFields, agg.over);
506
+ const slot = assertBound(`${context} select ${column.name}`, varFields, agg.over);
507
+ const roster = rosterOf(slot.field);
508
+ if (roster !== undefined) {
509
+ throw closedOrderError(`${context} select ${column.name}`, `the ${agg.fold} input ${agg.over}`, roster.name);
510
+ }
416
511
  return;
417
512
  }
418
513
  assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration);
419
514
  return;
420
515
  }
421
- case "arg":
516
+ case "arg": {
422
517
  assertBound(`${context} select ${column.name}`, varFields, agg.over);
423
- assertBound(`${context} select ${column.name}`, varFields, agg.key);
518
+ const key = assertBound(`${context} select ${column.name}`, varFields, agg.key);
519
+ const keyRoster = rosterOf(key.field);
520
+ if (keyRoster !== undefined) {
521
+ throw closedOrderError(`${context} select ${column.name}`, `the ${agg.direction} key ${agg.key}`, keyRoster.name);
522
+ }
424
523
  return;
524
+ }
425
525
  case "pack":
426
526
  assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over);
427
527
  return;
428
528
  }
429
529
  }
530
+ /**
531
+ * Resolves the roster one select column decodes through: a projected var,
532
+ * or an Arg-carried payload, bound at a closed-referencing field carries
533
+ * that field's roster (read off `varFields` — the same slot the domain
534
+ * machinery reads), and `decodeAnswers` lifts the column's row ids back to
535
+ * handle NAMES through it — the runtime twin of the row type's `Infer`
536
+ * claim. Every other entry decodes bare: counts are counts, the measure
537
+ * and `pack` are never closed, and a closed FOLD is banned outright
538
+ * ({@link closedOrderError}) before this resolution runs.
539
+ */
540
+ function selectClosedOf(varFields, entry) {
541
+ let over;
542
+ if (entry.kind === "var") {
543
+ over = entry.over;
544
+ }
545
+ else if (entry.kind === "aggregate" && entry.agg.op === "arg") {
546
+ over = entry.agg.over;
547
+ }
548
+ else {
549
+ over = undefined;
550
+ }
551
+ if (over === undefined) {
552
+ return undefined;
553
+ }
554
+ return rosterOf(varFields[over]?.field);
555
+ }
430
556
  /**
431
557
  * Completes one rule: classifies the select record (written order = answer
432
558
  * column order, names must be declaration-order-safe keys), and validates
@@ -485,7 +611,13 @@ function completeRule(context, state, columns) {
485
611
  }
486
612
  return Object.freeze({
487
613
  items: state.items,
488
- select: Object.freeze([...columns]),
614
+ select: Object.freeze(columns.map(function enrichColumn(column) {
615
+ return Object.freeze({
616
+ name: column.name,
617
+ entry: column.entry,
618
+ closed: selectClosedOf(state.varFields, column.entry)
619
+ });
620
+ })),
489
621
  varFields: state.varFields,
490
622
  paramUses: state.paramUses
491
623
  });
@@ -561,10 +693,12 @@ function makeRawScope(context) {
561
693
  return scope;
562
694
  }
563
695
  /**
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
696
+ * The rule builders' trusted admission seam THE home of the
697
+ * trusted-admission-seam pattern the other mint guards cite (the face,
698
+ * class-map, axiom-readback, rec-handle, and query-value seams): the raw
699
+ * builder is one runtime shape for every context, and this guard verifies
700
+ * the checkable fact — the builder verbs exist — before the value is
701
+ * admitted at its TYPED face. The type-level
568
702
  * judgments (domain-equal joins, boundness, the recursion cut) live in the
569
703
  * interfaces themselves; the runtime twin of every one of them is a
570
704
  * construction-time validation in this module.
@@ -588,6 +722,10 @@ function makeOutputRuleScope(program) {
588
722
  }
589
723
  return raw;
590
724
  }
725
+ /** Renders one head column's closed slice for the rule-alignment check's diagnostics. */
726
+ function renderClosedSlice(closed) {
727
+ return closed === undefined ? "a bare value" : `a ${closed.name} reference`;
728
+ }
591
729
  /** Renders one head column's signature for the rule-alignment check. */
592
730
  function headSignature(column) {
593
731
  const entry = column.entry;
@@ -603,11 +741,48 @@ function headSignature(column) {
603
741
  }
604
742
  return `${column.name}:${agg.op}`;
605
743
  }
744
+ /**
745
+ * The classed slot one answer column's VALUES flow from, resolved through
746
+ * the rule's own binding environment: a projected var's first-binding slot,
747
+ * or an Arg-carried payload's (`argMax`/`argMin` carry `over` verbatim —
748
+ * the same two shapes the closed slice lifts). Counts, folds, `pack` and
749
+ * the measure derive numbers/intervals rather than carrying a slot's ids,
750
+ * so they resolve no slot (`undefined`).
751
+ */
752
+ function headSlotOf(rule, column) {
753
+ const entry = column.entry;
754
+ if (entry.kind === "var") {
755
+ return rule.varFields[entry.over];
756
+ }
757
+ if (entry.kind === "aggregate" && entry.agg.op === "arg") {
758
+ return rule.varFields[entry.agg.over];
759
+ }
760
+ return undefined;
761
+ }
762
+ /** The roster a param anchor carries: present exactly on a closed-reference field anchor (rides THE one `rosterOf` reader). */
763
+ function anchorRosterOf(anchor) {
764
+ return anchor === "measure" ? undefined : rosterOf(anchor);
765
+ }
766
+ /** Renders one param anchor's closedness for the registry's coherence diagnostics. */
767
+ function renderParamAnchor(roster) {
768
+ return roster === undefined ? "a non-closed position" : `a ${roster.name} reference`;
769
+ }
606
770
  /**
607
771
  * Folds every rule's param uses (recs in declaration order first, output
608
772
  * rules last — exactly the lowering walk) into the query's registry:
609
773
  * first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
610
- * the wire, and one name must keep one shape.
774
+ * the wire, and one name must keep one shape AND one closedness — every
775
+ * anchored use of one name must agree on the roster (value identity), so a
776
+ * param anchored at a closed reference is GUARANTEED to ride the one
777
+ * roster-verification point (`taggedHandleId`) at execute; a name anchored
778
+ * both at a closed reference and at a non-closed position (or at two
779
+ * vocabularies) is refused here, because the wire would translate only the
780
+ * first anchor's reading (the type tier intersects the uses to `never`;
781
+ * this is its runtime twin for untyped callers). A param whose anchor is a
782
+ * CLOSED reference must never sit in an order-comparison position — the
783
+ * anchor types its value a handle name and the engine would order the
784
+ * translated row ids, so the pairing is refused here too (the registry is
785
+ * the one place a name's every use and its anchoring field meet).
611
786
  */
612
787
  function paramRegistryOf(recs, rules) {
613
788
  const order = [];
@@ -617,16 +792,35 @@ function paramRegistryOf(recs, rules) {
617
792
  const existing = byName.get(use.name);
618
793
  if (existing === undefined) {
619
794
  order.push(use.name);
620
- byName.set(use.name, { shape: use.shape, anchor: use.anchor, op: use.op });
795
+ byName.set(use.name, {
796
+ shape: use.shape,
797
+ anchor: use.anchor,
798
+ op: use.op,
799
+ members: use.members,
800
+ orderOp: isOrderOp(use.op) ? use.op : undefined
801
+ });
621
802
  continue;
622
803
  }
804
+ if ((existing.members === undefined) !== (use.members === undefined)) {
805
+ throw errors.new(`query param ${use.name} collides with a membership array's registry entry — name the param differently`);
806
+ }
623
807
  if (existing.shape !== use.shape) {
624
808
  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
809
  }
810
+ if (existing.anchor !== undefined && use.anchor !== undefined) {
811
+ const registered = anchorRosterOf(existing.anchor);
812
+ const anchored = anchorRosterOf(use.anchor);
813
+ if (registered !== anchored) {
814
+ 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`);
815
+ }
816
+ }
626
817
  if (existing.anchor === undefined && use.anchor !== undefined) {
627
818
  existing.anchor = use.anchor;
628
819
  existing.op = use.op;
629
820
  }
821
+ if (existing.orderOp === undefined && isOrderOp(use.op)) {
822
+ existing.orderOp = use.op;
823
+ }
630
824
  }
631
825
  }
632
826
  for (const rec of recs) {
@@ -642,7 +836,11 @@ function paramRegistryOf(recs, rules) {
642
836
  if (entry === undefined) {
643
837
  throw errors.new(`query param ${name} lost its registry entry`);
644
838
  }
645
- return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op });
839
+ const anchorRoster = anchorRosterOf(entry.anchor);
840
+ if (entry.orderOp !== undefined && anchorRoster !== undefined) {
841
+ throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name);
842
+ }
843
+ return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members });
646
844
  }));
647
845
  }
648
846
  /**
@@ -662,6 +860,32 @@ function makeRawQuery(theory, recs, rules) {
662
860
  if (candidate !== signature) {
663
861
  throw errors.new(`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`);
664
862
  }
863
+ // The closed slice is part of the head too: one answer column decodes
864
+ // through one roster, so a union whose rules bind a column at
865
+ // different vocabularies (or one closed, one bare — the ids would
866
+ // mistranslate silently) is refused pointed. Vocabulary identity is
867
+ // value identity, the SDK's membership rule everywhere.
868
+ rule.select.forEach(function verifyClosedSlice(column, position) {
869
+ const lead = first.select[position];
870
+ if (lead !== undefined && column.closed !== lead.closed) {
871
+ throw errors.new(`every rule of a query derives the same head — the answer column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`);
872
+ }
873
+ // The law-class wall on the union head: one answer column is one
874
+ // value space, so the classed slot each rule binds the column at
875
+ // must join across rules — the SAME fieldJoins judgment every
876
+ // join/eq/negated-atom position enforces. The SDK holds this wall
877
+ // because the wire IR carries no domains: the engine cannot
878
+ // backstop it, and without it a union mixes (say) Holder ids and
879
+ // Account ids in one column the consumer reads as one id space.
880
+ if (lead === undefined) {
881
+ return;
882
+ }
883
+ const leadSlot = headSlotOf(first, lead);
884
+ const slot = headSlotOf(rule, column);
885
+ if (leadSlot !== undefined && slot !== undefined && !fieldJoins(leadSlot, slot)) {
886
+ throw errors.new(`every rule of a query derives the same head — the answer column ${lead.name} unions domain-unequal fields: bound at ${renderFieldKind(leadSlot)} in rule 0 but at ${renderFieldKind(slot)} in rule ${index} (a union column joins only class-equal slots; bare pairs only with bare)`);
887
+ }
888
+ });
665
889
  });
666
890
  const data = Object.freeze({
667
891
  recs: Object.freeze([...recs]),
@@ -681,7 +905,7 @@ function makeRawQuery(theory, recs, rules) {
681
905
  return value;
682
906
  }
683
907
  /**
684
- * The query values' trusted admission seam (the `refsComplete` pattern):
908
+ * The query values' trusted admission seam (the {@link isTypedScope} pattern):
685
909
  * the checkable fact — the value was assembled over the identical theory —
686
910
  * is verified before the raw value is admitted at its typed face.
687
911
  */
@@ -713,33 +937,25 @@ function query(theory) {
713
937
  Object.freeze(start);
714
938
  return start;
715
939
  }
716
- /** The typed shape refusal of the literal tagger — a genuine failure, never data. */
717
- function literalShapeError(context, expected, value) {
718
- return errors.new(`${context}: expected ${expected}, got ${typeof value}`);
719
- }
720
- /** Narrows an interval-shaped literal (a plain `{ start, end }` bigint pair). */
721
- function isIntervalShaped(value) {
722
- return (typeof value === "object" &&
723
- value !== null &&
724
- "start" in value &&
725
- "end" in value &&
726
- typeof value.start === "bigint" &&
727
- typeof value.end === "bigint");
728
- }
729
940
  /**
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.
941
+ * Tags one closed-reference literal: the handle NAME, verified against the
942
+ * roster (the belt the wide fallback type cannot provide — structural
943
+ * values make any string spellable here) and translated to its
944
+ * declaration-order row id, tagged u64 — queries cross ids, never handle
945
+ * names; the wire is untouched. THE single roster-verification point of
946
+ * the query surface: atom-binding literals, comparison literals,
947
+ * execute-time params, and membership-array members all reach it (never
948
+ * duplicate the check per call site).
734
949
  */
735
950
  function taggedHandleId(context, closed, value) {
736
- if (typeof value !== "bigint") {
737
- throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value);
951
+ if (typeof value !== "string") {
952
+ throw literalShapeError(context, `a ${closed.name} handle name (string)`, value);
738
953
  }
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})`);
954
+ const id = closed.handles.indexOf(value);
955
+ if (id < 0) {
956
+ throw errors.new(`${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`);
741
957
  }
742
- return { kind: "u64", value };
958
+ return { kind: "u64", value: BigInt(id) };
743
959
  }
744
960
  /**
745
961
  * Tags one literal in an interval element domain: a bigint tags as the
@@ -753,7 +969,7 @@ function taggedAtElementDomain(context, element, value) {
753
969
  }
754
970
  return { kind: "i64", value };
755
971
  }
756
- if (isIntervalShaped(value)) {
972
+ if (isIntervalValue(value)) {
757
973
  if (element === "u64") {
758
974
  return { kind: "intervalU64", start: value.start, end: value.end };
759
975
  }
@@ -770,8 +986,9 @@ function taggedAtElementDomain(context, element, value) {
770
986
  * tagged u64 after a roster verification.
771
987
  */
772
988
  function taggedLiteral(context, field, value) {
773
- if ("closed" in field) {
774
- return taggedHandleId(context, field.closed, value);
989
+ const roster = rosterOf(field);
990
+ if (roster !== undefined) {
991
+ return taggedHandleId(context, roster, value);
775
992
  }
776
993
  switch (field.kind) {
777
994
  case "bool": {
@@ -796,6 +1013,18 @@ function taggedLiteral(context, field, value) {
796
1013
  if (typeof value !== "string") {
797
1014
  throw literalShapeError(context, "string", value);
798
1015
  }
1016
+ /**
1017
+ * The marshal's bijection law at the query seam (`marshal.ts`
1018
+ * cellOf): a lone surrogate would be lossily replaced with
1019
+ * U+FFFD at the bridge's UTF-8 crossing and silently match a
1020
+ * fact the typed write surface can never store — distinct JS
1021
+ * strings collapsing to one wire query. This is the single
1022
+ * seam every query string literal, string param
1023
+ * (`taggedCmpLiteral`), and membership member lowers through.
1024
+ */
1025
+ if (!value.isWellFormed()) {
1026
+ throw literalShapeError(context, "well-formed string", value);
1027
+ }
799
1028
  return { kind: "string", value };
800
1029
  }
801
1030
  case "bytes": {
@@ -812,11 +1041,11 @@ function taggedLiteral(context, field, value) {
812
1041
  * Tags one host literal at a COMPARISON or PARAM position, where the
813
1042
  * SIBLING anchors the type: a measure sibling is u64, an interval-field
814
1043
  * sibling contributes its element domain (so both a point literal in
815
- * `covers` and a `span` literal in `allen` tag correctly), a scalar
1044
+ * `pointIn` and a `span` literal in `allen` tag correctly), a scalar
816
1045
  * sibling its own type. At `pointIn` the operand order is interval-left,
817
1046
  * 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
1047
+ * beside a scalar element-typed sibling is the LEGAL interval operand of
1048
+ * `pointIn(t, span(...))` and tags as the interval of the sibling's
820
1049
  * element domain; under every other operator an interval shape against a
821
1050
  * scalar sibling stays refused (the engine's IllegalComparison — the
822
1051
  * bug-hunt fix, preserved op-aware).
@@ -828,13 +1057,13 @@ function taggedCmpLiteral(context, sibling, value, op) {
828
1057
  }
829
1058
  return { kind: "u64", value };
830
1059
  }
831
- if (!("closed" in sibling) && sibling.kind === "interval") {
1060
+ if (rosterOf(sibling) === undefined && sibling.kind === "interval") {
832
1061
  return taggedAtElementDomain(context, sibling.element, value);
833
1062
  }
834
1063
  if (op === "pointIn" &&
835
- !("closed" in sibling) &&
1064
+ rosterOf(sibling) === undefined &&
836
1065
  (sibling.kind === "u64" || sibling.kind === "i64") &&
837
- isIntervalShaped(value)) {
1066
+ isIntervalValue(value)) {
838
1067
  return taggedAtElementDomain(context, sibling.kind, value);
839
1068
  }
840
1069
  return taggedLiteral(context, sibling, value);
@@ -866,7 +1095,7 @@ function paramIdOf(ctx, name) {
866
1095
  * Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
867
1096
  * same edb source — its ordinal is its record-declaration slot exactly like
868
1097
  * an ordinary relation's — with field ordinals over the SEALED shape: `id`
869
- * at 0, each payload column at its declared index + 1 (`matchFieldsOf`
1098
+ * at 0, each payload column at its declared index + 1 (`sealedFieldsOf`
870
1099
  * carries the shift; the lowering golden pins it).
871
1100
  */
872
1101
  function lowerAtom(ctx, atom, ids) {
@@ -878,7 +1107,7 @@ function lowerAtom(ctx, atom, ids) {
878
1107
  if (relationId === undefined) {
879
1108
  throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`);
880
1109
  }
881
- const ordered = matchFieldsOf(atom.relation);
1110
+ const ordered = sealedFieldsOf(atom.relation);
882
1111
  const bindings = atom.bindings.map(function lowerBinding(binding) {
883
1112
  const ordinal = ordered.findIndex(function byName(candidate) {
884
1113
  return candidate.name === binding.field;
@@ -890,7 +1119,12 @@ function lowerAtom(ctx, atom, ids) {
890
1119
  });
891
1120
  return { source: { kind: "edb", relation: relationId }, bindings };
892
1121
  }
893
- /** Lowers one binding term. */
1122
+ /**
1123
+ * Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
1124
+ * existing param-set term over its content-addressed registry entry — the
1125
+ * program IR is byte-identical to the same set spelled `r.inSet`; the SDK
1126
+ * supplies the translated member set itself at execute (`wireParams`).
1127
+ */
894
1128
  function lowerBindingTerm(ctx, context, binding, ids) {
895
1129
  const bound = binding.term;
896
1130
  switch (bound.kind) {
@@ -900,6 +1134,8 @@ function lowerBindingTerm(ctx, context, binding, ids) {
900
1134
  return { kind: "param", param: paramIdOf(ctx, bound.name) };
901
1135
  case "setParam":
902
1136
  return { kind: "paramSet", param: paramIdOf(ctx, bound.name) };
1137
+ case "literalSet":
1138
+ return { kind: "paramSet", param: paramIdOf(ctx, bound.name) };
903
1139
  case "literal":
904
1140
  return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) };
905
1141
  }