@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
@@ -26,8 +26,9 @@
26
26
  */
27
27
 
28
28
  import * as errors from "@superbuilders/errors"
29
- import type { AnyField } from "#fields.ts"
30
- import { assertDeclarationOrderKey } from "#fields.ts"
29
+ import { sealedFieldsOf } from "#closed.ts"
30
+ import type { AnyField, ClosedRoster } from "#fields.ts"
31
+ import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
31
32
  import type { ClassRecordOf, SchemaClasses } from "#law.ts"
32
33
  import type {
33
34
  AtomIr,
@@ -68,7 +69,7 @@ import type {
68
69
  SelectEntryData,
69
70
  TreeData
70
71
  } from "#query/atom.ts"
71
- import { allen, and, covers, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
72
+ import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
72
73
  import type {
73
74
  ClassedField,
74
75
  EnvShape,
@@ -88,13 +89,12 @@ import {
88
89
  makeParam,
89
90
  makeSetParam,
90
91
  makeVar,
91
- makeVars,
92
92
  renderFieldKind,
93
93
  term
94
94
  } from "#query/scope.ts"
95
95
  import type { CheckNameSelect, CheckSelect, RowOfNameSelect, RowOfSelect, SelectEntry } from "#query/select.ts"
96
96
  import { argMax, argMin, count, countDistinct, max, min, pack, sum } from "#query/select.ts"
97
- import type { FieldsShape, RelationField } from "#relation.ts"
97
+ import type { FieldsShape } from "#relation.ts"
98
98
  import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
99
99
 
100
100
  /**
@@ -198,8 +198,6 @@ type CheckIdbVars<Env extends EnvShape, V, Head extends HeadShape = undefined> =
198
198
  interface TermOps {
199
199
  /** Declares/names one variable: typed by the field it first binds; reuse joins. */
200
200
  readonly var: typeof makeVar
201
- /** Mints several variables at once — `const { service, w } = r.vars("service", "w")`: each name typed exactly; duplicates refuse. */
202
- readonly vars: typeof makeVars
203
201
  /** Names one scalar parameter: typed by its use; the key of the execute params object. */
204
202
  readonly param: typeof makeParam
205
203
  /** Names one ∈-set parameter (the IR's `ParamSet`): bound to a readonly array at execution. */
@@ -215,7 +213,6 @@ interface TermOps {
215
213
  readonly gt: typeof gt
216
214
  readonly ge: typeof ge
217
215
  readonly pointIn: typeof pointIn
218
- readonly covers: typeof covers
219
216
  readonly allen: typeof allen
220
217
  readonly and: typeof and
221
218
  readonly or: typeof or
@@ -431,7 +428,6 @@ interface QueryStart<Rels extends SchemaRelations, Classes extends SchemaClasses
431
428
  /** The frozen constructor vocabulary every rule builder spreads. */
432
429
  const termOps: TermOps = Object.freeze({
433
430
  var: makeVar,
434
- vars: makeVars,
435
431
  param: makeParam,
436
432
  inSet: makeSetParam,
437
433
  maskParam: makeMaskParam,
@@ -443,7 +439,6 @@ const termOps: TermOps = Object.freeze({
443
439
  gt,
444
440
  ge,
445
441
  pointIn,
446
- covers,
447
442
  allen,
448
443
  and,
449
444
  or,
@@ -480,19 +475,54 @@ interface ResolvedBindings {
480
475
  }
481
476
 
482
477
  /**
483
- * The ordered matchable fields of an atom ownerthe runtime twin of the
484
- * type tier's `MatchFields`: a relation's declared fields; a closed
485
- * relation's SEALED shape, the synthetic `id` (the value's own
486
- * roster-carrying descriptor, by identity) at ordinal 0 and the declared
487
- * payload columns at declared index + 1 (the sealed shift, mirroring
488
- * `spec.rs`'s resolver a `ClosedColumn` is structurally a
489
- * {@link RelationField}). The lowering golden pins this mapping.
478
+ * Judges one membership ARRAY at a binding positionlegal exactly at a
479
+ * CLOSED-reference field (the owner ruling: ordinary u64/str membership is
480
+ * spelled through `r.inSet` params; literal arrays are the closed
481
+ * vocabulary's spelling), holding 2 DISTINCT handle names (the
482
+ * degenerate sets are refusals: empty selects nothing, one element is the
483
+ * bare literal respelled, and a duplicate member is the same respelling in
484
+ * disguise — write each member once). The returned name is
485
+ * CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
486
+ * so two spellings of one set, reordered or not, share one dense
487
+ * `ParamId`); the members are shape-checked strings here and
488
+ * roster-verified at the one verification point (`taggedHandleId`) when
489
+ * the SDK supplies the set at execute — the same moment a bound `r.inSet`
490
+ * param's members are judged.
490
491
  */
491
- function matchFieldsOf(owner: MatchOwner): readonly RelationField[] {
492
- if ("axioms" in owner) {
493
- return [Object.freeze({ name: "id", field: owner.id }), ...owner.data.columns]
492
+ function membershipSet(
493
+ context: string,
494
+ field: AnyField,
495
+ value: readonly unknown[]
496
+ ): { readonly name: string; readonly members: readonly string[] } {
497
+ const roster = rosterOf(field)
498
+ if (roster === undefined) {
499
+ throw errors.new(
500
+ `${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`
501
+ )
494
502
  }
495
- return owner.data.fields
503
+ if (value.length === 0) {
504
+ throw errors.new(`${context}: an empty membership array selects nothing — write the query you mean`)
505
+ }
506
+ if (value.length === 1) {
507
+ throw errors.new(
508
+ `${context}: a one-element membership array is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`
509
+ )
510
+ }
511
+ const seen = new Set<string>()
512
+ const members = value.map(function memberName(member) {
513
+ if (typeof member !== "string") {
514
+ throw literalShapeError(context, `a ${roster.name} handle name (string)`, member)
515
+ }
516
+ if (seen.has(member)) {
517
+ throw errors.new(
518
+ `${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
519
+ )
520
+ }
521
+ seen.add(member)
522
+ return member
523
+ })
524
+ const key = [...members].sort()
525
+ return { name: `∈ ${roster.name} ${JSON.stringify(key)}`, members: Object.freeze(members) }
496
526
  }
497
527
 
498
528
  /**
@@ -514,7 +544,7 @@ function resolveBindings(
514
544
  const vars: Array<{ readonly name: string; readonly slot: ClassedField }> = []
515
545
  const uses: ParamUse[] = []
516
546
  const relationClasses = classes[relation.name]
517
- const ordered = matchFieldsOf(relation)
547
+ const ordered = sealedFieldsOf(relation)
518
548
  for (const [fieldName, value] of Object.entries(bindings)) {
519
549
  if (value === undefined) {
520
550
  continue
@@ -539,14 +569,26 @@ function resolveBindings(
539
569
  case "param": {
540
570
  bound = Object.freeze({ kind: "param" as const, name: value.name })
541
571
  uses.push(
542
- Object.freeze({ name: value.name, shape: "value" as const, anchor: declared.field, op: "binding" as const })
572
+ Object.freeze({
573
+ name: value.name,
574
+ shape: "value" as const,
575
+ anchor: declared.field,
576
+ op: "binding" as const,
577
+ members: undefined
578
+ })
543
579
  )
544
580
  break
545
581
  }
546
582
  case "setParam": {
547
583
  bound = Object.freeze({ kind: "setParam" as const, name: value.name })
548
584
  uses.push(
549
- Object.freeze({ name: value.name, shape: "set" as const, anchor: declared.field, op: "binding" as const })
585
+ Object.freeze({
586
+ name: value.name,
587
+ shape: "set" as const,
588
+ anchor: declared.field,
589
+ op: "binding" as const,
590
+ members: undefined
591
+ })
550
592
  )
551
593
  break
552
594
  }
@@ -559,6 +601,18 @@ function resolveBindings(
559
601
  `${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`
560
602
  )
561
603
  }
604
+ } else if (Array.isArray(value)) {
605
+ const set = membershipSet(`${context}.${fieldName}`, declared.field, value)
606
+ bound = Object.freeze({ kind: "literalSet" as const, name: set.name, members: set.members })
607
+ uses.push(
608
+ Object.freeze({
609
+ name: set.name,
610
+ shape: "set" as const,
611
+ anchor: declared.field,
612
+ op: "binding" as const,
613
+ members: set.members
614
+ })
615
+ )
562
616
  } else {
563
617
  bound = Object.freeze({ kind: "literal" as const, value })
564
618
  }
@@ -651,7 +705,8 @@ function sideUses(
651
705
  name: side.name,
652
706
  shape: side.kind === "param" ? ("value" as const) : ("set" as const),
653
707
  anchor,
654
- op
708
+ op,
709
+ members: undefined
655
710
  })
656
711
  )
657
712
  }
@@ -671,7 +726,13 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedFie
671
726
  } else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
672
727
  mask = Object.freeze({ kind: "param" as const, name: maskValue.name })
673
728
  uses.push(
674
- Object.freeze({ name: maskValue.name, shape: "mask" as const, anchor: undefined, op: "allen" as const })
729
+ Object.freeze({
730
+ name: maskValue.name,
731
+ shape: "mask" as const,
732
+ anchor: undefined,
733
+ op: "allen" as const,
734
+ members: undefined
735
+ })
675
736
  )
676
737
  } else {
677
738
  throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
@@ -742,14 +803,27 @@ function isAggregateEntry(
742
803
  return typeof value === "object" && value !== null && "agg" in value
743
804
  }
744
805
 
745
- /** Classifies one select entry into its named answer column. */
806
+ /**
807
+ * Classifies one select entry into its named answer column. The `closed`
808
+ * slice is resolved LATER, at rule completion (`completeRule`), where the
809
+ * rule's `varFields` are in hand — until then every column is provisionally
810
+ * bare.
811
+ */
746
812
  function selectColumnOf(entry: unknown): SelectColumn {
747
813
  if (typeof entry === "string") {
748
- return Object.freeze({ name: entry, entry: Object.freeze({ kind: "var" as const, over: entry }) })
814
+ return Object.freeze({
815
+ name: entry,
816
+ entry: Object.freeze({ kind: "var" as const, over: entry }),
817
+ closed: undefined
818
+ })
749
819
  }
750
820
  if (isTerm(entry)) {
751
821
  if (entry[term] === "duration") {
752
- return Object.freeze({ name: entry.name, entry: Object.freeze({ kind: "measure" as const, over: entry.name }) })
822
+ return Object.freeze({
823
+ name: entry.name,
824
+ entry: Object.freeze({ kind: "measure" as const, over: entry.name }),
825
+ closed: undefined
826
+ })
753
827
  }
754
828
  throw errors.new(
755
829
  `query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`
@@ -768,7 +842,11 @@ function aggregateColumnOf(entry: {
768
842
  readonly key: unknown
769
843
  }): SelectColumn {
770
844
  function column(name: string, agg: AggData): SelectColumn {
771
- return Object.freeze({ name, entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }) })
845
+ return Object.freeze({
846
+ name,
847
+ entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }),
848
+ closed: undefined
849
+ })
772
850
  }
773
851
  const over = entry.over
774
852
  switch (entry.agg) {
@@ -809,6 +887,26 @@ function aggregateColumnOf(entry: {
809
887
  }
810
888
  }
811
889
 
890
+ /**
891
+ * The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
892
+ * § orderability): a closed reference is equality-and-membership only —
893
+ * its declaration-id order is an encoding accident, so every
894
+ * order-comparison and fold position refuses it. The construction-time
895
+ * twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
896
+ * untyped callers too (the engine cannot backstop this one: the wire IR
897
+ * carries plain u64s, no rosters).
898
+ */
899
+ function closedOrderError(context: string, position: string, vocabulary: string): Error {
900
+ return errors.new(
901
+ `${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)`
902
+ )
903
+ }
904
+
905
+ /** The comparison ops under the orderable ban (order roster + point membership — every order-comparison position). */
906
+ function isOrderOp(op: CmpKind | "binding"): op is "lt" | "le" | "gt" | "ge" | "pointIn" {
907
+ return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn"
908
+ }
909
+
812
910
  /** Requires a var name to be bound by a relation atom of the rule. */
813
911
  function assertBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): ClassedField {
814
912
  const slot = varFields[name]
@@ -841,7 +939,11 @@ function validateCond(context: string, varFields: Readonly<Record<string, Classe
841
939
  if (cond.kind === "cmp") {
842
940
  for (const side of [cond.lhs, cond.rhs]) {
843
941
  if (side.kind === "var") {
844
- assertBound(context, varFields, side.name)
942
+ const slot = assertBound(context, varFields, side.name)
943
+ const roster = rosterOf(slot.field)
944
+ if (isOrderOp(cond.op) && roster !== undefined) {
945
+ throw closedOrderError(context, `the ${cond.op} side ${side.name}`, roster.name)
946
+ }
845
947
  }
846
948
  if (side.kind === "measure") {
847
949
  assertIntervalBound(context, varFields, side.name)
@@ -887,22 +989,63 @@ function validateColumn(
887
989
  return
888
990
  case "fold": {
889
991
  if (typeof agg.over === "string") {
890
- assertBound(`${context} select ${column.name}`, varFields, agg.over)
992
+ const slot = assertBound(`${context} select ${column.name}`, varFields, agg.over)
993
+ const roster = rosterOf(slot.field)
994
+ if (roster !== undefined) {
995
+ throw closedOrderError(`${context} select ${column.name}`, `the ${agg.fold} input ${agg.over}`, roster.name)
996
+ }
891
997
  return
892
998
  }
893
999
  assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration)
894
1000
  return
895
1001
  }
896
- case "arg":
1002
+ case "arg": {
897
1003
  assertBound(`${context} select ${column.name}`, varFields, agg.over)
898
- assertBound(`${context} select ${column.name}`, varFields, agg.key)
1004
+ const key = assertBound(`${context} select ${column.name}`, varFields, agg.key)
1005
+ const keyRoster = rosterOf(key.field)
1006
+ if (keyRoster !== undefined) {
1007
+ throw closedOrderError(
1008
+ `${context} select ${column.name}`,
1009
+ `the ${agg.direction} key ${agg.key}`,
1010
+ keyRoster.name
1011
+ )
1012
+ }
899
1013
  return
1014
+ }
900
1015
  case "pack":
901
1016
  assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over)
902
1017
  return
903
1018
  }
904
1019
  }
905
1020
 
1021
+ /**
1022
+ * Resolves the roster one select column decodes through: a projected var,
1023
+ * or an Arg-carried payload, bound at a closed-referencing field carries
1024
+ * that field's roster (read off `varFields` — the same slot the domain
1025
+ * machinery reads), and `decodeAnswers` lifts the column's row ids back to
1026
+ * handle NAMES through it — the runtime twin of the row type's `Infer`
1027
+ * claim. Every other entry decodes bare: counts are counts, the measure
1028
+ * and `pack` are never closed, and a closed FOLD is banned outright
1029
+ * ({@link closedOrderError}) before this resolution runs.
1030
+ */
1031
+ function selectClosedOf(
1032
+ varFields: Readonly<Record<string, ClassedField>>,
1033
+ entry: SelectEntryData
1034
+ ): ClosedRoster | undefined {
1035
+ let over: string | undefined
1036
+ if (entry.kind === "var") {
1037
+ over = entry.over
1038
+ } else if (entry.kind === "aggregate" && entry.agg.op === "arg") {
1039
+ over = entry.agg.over
1040
+ } else {
1041
+ over = undefined
1042
+ }
1043
+ if (over === undefined) {
1044
+ return undefined
1045
+ }
1046
+ return rosterOf(varFields[over]?.field)
1047
+ }
1048
+
906
1049
  /**
907
1050
  * Completes one rule: classifies the select record (written order = answer
908
1051
  * column order, names must be declaration-order-safe keys), and validates
@@ -969,7 +1112,15 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
969
1112
  }
970
1113
  return Object.freeze({
971
1114
  items: state.items,
972
- select: Object.freeze([...columns]),
1115
+ select: Object.freeze(
1116
+ columns.map(function enrichColumn(column): SelectColumn {
1117
+ return Object.freeze({
1118
+ name: column.name,
1119
+ entry: column.entry,
1120
+ closed: selectClosedOf(state.varFields, column.entry)
1121
+ })
1122
+ })
1123
+ ),
973
1124
  varFields: state.varFields,
974
1125
  paramUses: state.paramUses
975
1126
  })
@@ -1091,10 +1242,12 @@ function makeRawScope(context: ChainContext): RawScope {
1091
1242
  }
1092
1243
 
1093
1244
  /**
1094
- * The one trusted admission seam of the rule builders (the `relation()`
1095
- * `refsComplete` pattern): the raw builder is one runtime shape for every
1096
- * context, and this guard verifies the checkable fact — the builder verbs
1097
- * exist before the value is admitted at its TYPED face. The type-level
1245
+ * The rule builders' trusted admission seam THE home of the
1246
+ * trusted-admission-seam pattern the other mint guards cite (the face,
1247
+ * class-map, axiom-readback, rec-handle, and query-value seams): the raw
1248
+ * builder is one runtime shape for every context, and this guard verifies
1249
+ * the checkable fact — the builder verbs exist — before the value is
1250
+ * admitted at its TYPED face. The type-level
1098
1251
  * judgments (domain-equal joins, boundness, the recursion cut) live in the
1099
1252
  * interfaces themselves; the runtime twin of every one of them is a
1100
1253
  * construction-time validation in this module.
@@ -1132,6 +1285,11 @@ interface ProgramState {
1132
1285
  sealed: boolean
1133
1286
  }
1134
1287
 
1288
+ /** Renders one head column's closed slice for the rule-alignment check's diagnostics. */
1289
+ function renderClosedSlice(closed: ClosedRoster | undefined): string {
1290
+ return closed === undefined ? "a bare value" : `a ${closed.name} reference`
1291
+ }
1292
+
1135
1293
  /** Renders one head column's signature for the rule-alignment check. */
1136
1294
  function headSignature(column: SelectColumn): string {
1137
1295
  const entry = column.entry
@@ -1148,32 +1306,104 @@ function headSignature(column: SelectColumn): string {
1148
1306
  return `${column.name}:${agg.op}`
1149
1307
  }
1150
1308
 
1309
+ /**
1310
+ * The classed slot one answer column's VALUES flow from, resolved through
1311
+ * the rule's own binding environment: a projected var's first-binding slot,
1312
+ * or an Arg-carried payload's (`argMax`/`argMin` carry `over` verbatim —
1313
+ * the same two shapes the closed slice lifts). Counts, folds, `pack` and
1314
+ * the measure derive numbers/intervals rather than carrying a slot's ids,
1315
+ * so they resolve no slot (`undefined`).
1316
+ */
1317
+ function headSlotOf(rule: RuleData, column: SelectColumn): ClassedField | undefined {
1318
+ const entry = column.entry
1319
+ if (entry.kind === "var") {
1320
+ return rule.varFields[entry.over]
1321
+ }
1322
+ if (entry.kind === "aggregate" && entry.agg.op === "arg") {
1323
+ return rule.varFields[entry.agg.over]
1324
+ }
1325
+ return undefined
1326
+ }
1327
+
1328
+ /** The roster a param anchor carries: present exactly on a closed-reference field anchor (rides THE one `rosterOf` reader). */
1329
+ function anchorRosterOf(anchor: AnyField | "measure" | undefined): ClosedRoster | undefined {
1330
+ return anchor === "measure" ? undefined : rosterOf(anchor)
1331
+ }
1332
+
1333
+ /** Renders one param anchor's closedness for the registry's coherence diagnostics. */
1334
+ function renderParamAnchor(roster: ClosedRoster | undefined): string {
1335
+ return roster === undefined ? "a non-closed position" : `a ${roster.name} reference`
1336
+ }
1337
+
1151
1338
  /**
1152
1339
  * Folds every rule's param uses (recs in declaration order first, output
1153
1340
  * rules last — exactly the lowering walk) into the query's registry:
1154
1341
  * first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
1155
- * the wire, and one name must keep one shape.
1342
+ * the wire, and one name must keep one shape AND one closedness — every
1343
+ * anchored use of one name must agree on the roster (value identity), so a
1344
+ * param anchored at a closed reference is GUARANTEED to ride the one
1345
+ * roster-verification point (`taggedHandleId`) at execute; a name anchored
1346
+ * both at a closed reference and at a non-closed position (or at two
1347
+ * vocabularies) is refused here, because the wire would translate only the
1348
+ * first anchor's reading (the type tier intersects the uses to `never`;
1349
+ * this is its runtime twin for untyped callers). A param whose anchor is a
1350
+ * CLOSED reference must never sit in an order-comparison position — the
1351
+ * anchor types its value a handle name and the engine would order the
1352
+ * translated row ids, so the pairing is refused here too (the registry is
1353
+ * the one place a name's every use and its anchoring field meet).
1156
1354
  */
1157
1355
  function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
1158
1356
  const order: string[] = []
1159
- const byName = new Map<string, { shape: ParamEntry["shape"]; anchor: ParamEntry["anchor"]; op: ParamEntry["op"] }>()
1357
+ const byName = new Map<
1358
+ string,
1359
+ {
1360
+ shape: ParamEntry["shape"]
1361
+ anchor: ParamEntry["anchor"]
1362
+ op: ParamEntry["op"]
1363
+ members: ParamEntry["members"]
1364
+ orderOp: "lt" | "le" | "gt" | "ge" | "pointIn" | undefined
1365
+ }
1366
+ >()
1160
1367
  function fold(uses: readonly ParamUse[]): void {
1161
1368
  for (const use of uses) {
1162
1369
  const existing = byName.get(use.name)
1163
1370
  if (existing === undefined) {
1164
1371
  order.push(use.name)
1165
- byName.set(use.name, { shape: use.shape, anchor: use.anchor, op: use.op })
1372
+ byName.set(use.name, {
1373
+ shape: use.shape,
1374
+ anchor: use.anchor,
1375
+ op: use.op,
1376
+ members: use.members,
1377
+ orderOp: isOrderOp(use.op) ? use.op : undefined
1378
+ })
1166
1379
  continue
1167
1380
  }
1381
+ if ((existing.members === undefined) !== (use.members === undefined)) {
1382
+ throw errors.new(
1383
+ `query param ${use.name} collides with a membership array's registry entry — name the param differently`
1384
+ )
1385
+ }
1168
1386
  if (existing.shape !== use.shape) {
1169
1387
  throw errors.new(
1170
1388
  `query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`
1171
1389
  )
1172
1390
  }
1391
+ if (existing.anchor !== undefined && use.anchor !== undefined) {
1392
+ const registered = anchorRosterOf(existing.anchor)
1393
+ const anchored = anchorRosterOf(use.anchor)
1394
+ if (registered !== anchored) {
1395
+ throw errors.new(
1396
+ `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`
1397
+ )
1398
+ }
1399
+ }
1173
1400
  if (existing.anchor === undefined && use.anchor !== undefined) {
1174
1401
  existing.anchor = use.anchor
1175
1402
  existing.op = use.op
1176
1403
  }
1404
+ if (existing.orderOp === undefined && isOrderOp(use.op)) {
1405
+ existing.orderOp = use.op
1406
+ }
1177
1407
  }
1178
1408
  }
1179
1409
  for (const rec of recs) {
@@ -1190,7 +1420,11 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
1190
1420
  if (entry === undefined) {
1191
1421
  throw errors.new(`query param ${name} lost its registry entry`)
1192
1422
  }
1193
- return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op })
1423
+ const anchorRoster = anchorRosterOf(entry.anchor)
1424
+ if (entry.orderOp !== undefined && anchorRoster !== undefined) {
1425
+ throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, anchorRoster.name)
1426
+ }
1427
+ return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members })
1194
1428
  })
1195
1429
  )
1196
1430
  }
@@ -1221,6 +1455,36 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
1221
1455
  `every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`
1222
1456
  )
1223
1457
  }
1458
+ // The closed slice is part of the head too: one answer column decodes
1459
+ // through one roster, so a union whose rules bind a column at
1460
+ // different vocabularies (or one closed, one bare — the ids would
1461
+ // mistranslate silently) is refused pointed. Vocabulary identity is
1462
+ // value identity, the SDK's membership rule everywhere.
1463
+ rule.select.forEach(function verifyClosedSlice(column, position) {
1464
+ const lead = first.select[position]
1465
+ if (lead !== undefined && column.closed !== lead.closed) {
1466
+ throw errors.new(
1467
+ `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)`
1468
+ )
1469
+ }
1470
+ // The law-class wall on the union head: one answer column is one
1471
+ // value space, so the classed slot each rule binds the column at
1472
+ // must join across rules — the SAME fieldJoins judgment every
1473
+ // join/eq/negated-atom position enforces. The SDK holds this wall
1474
+ // because the wire IR carries no domains: the engine cannot
1475
+ // backstop it, and without it a union mixes (say) Holder ids and
1476
+ // Account ids in one column the consumer reads as one id space.
1477
+ if (lead === undefined) {
1478
+ return
1479
+ }
1480
+ const leadSlot = headSlotOf(first, lead)
1481
+ const slot = headSlotOf(rule, column)
1482
+ if (leadSlot !== undefined && slot !== undefined && !fieldJoins(leadSlot, slot)) {
1483
+ throw errors.new(
1484
+ `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)`
1485
+ )
1486
+ }
1487
+ })
1224
1488
  })
1225
1489
  const data: QueryData = Object.freeze({
1226
1490
  recs: Object.freeze([...recs]),
@@ -1241,7 +1505,7 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
1241
1505
  }
1242
1506
 
1243
1507
  /**
1244
- * The query values' trusted admission seam (the `refsComplete` pattern):
1508
+ * The query values' trusted admission seam (the {@link isTypedScope} pattern):
1245
1509
  * the checkable fact — the value was assembled over the identical theory —
1246
1510
  * is verified before the raw value is admitted at its typed face.
1247
1511
  */
@@ -1287,43 +1551,31 @@ function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1287
1551
  return start
1288
1552
  }
1289
1553
 
1290
- /** The typed shape refusal of the literal tagger — a genuine failure, never data. */
1291
- function literalShapeError(context: string, expected: string, value: unknown): Error {
1292
- return errors.new(`${context}: expected ${expected}, got ${typeof value}`)
1293
- }
1294
-
1295
- /** Narrows an interval-shaped literal (a plain `{ start, end }` bigint pair). */
1296
- function isIntervalShaped(value: unknown): value is { readonly start: bigint; readonly end: bigint } {
1297
- return (
1298
- typeof value === "object" &&
1299
- value !== null &&
1300
- "start" in value &&
1301
- "end" in value &&
1302
- typeof value.start === "bigint" &&
1303
- typeof value.end === "bigint"
1304
- )
1305
- }
1306
-
1307
1554
  /**
1308
- * Tags one closed-reference literal: the bare handle id, verified against
1309
- * the roster (the belt the type level cannot provide — structural values
1310
- * make any bigint spellable here) and tagged u64 — queries cross ids,
1311
- * never handle names.
1555
+ * Tags one closed-reference literal: the handle NAME, verified against the
1556
+ * roster (the belt the wide fallback type cannot provide — structural
1557
+ * values make any string spellable here) and translated to its
1558
+ * declaration-order row id, tagged u64 — queries cross ids, never handle
1559
+ * names; the wire is untouched. THE single roster-verification point of
1560
+ * the query surface: atom-binding literals, comparison literals,
1561
+ * execute-time params, and membership-array members all reach it (never
1562
+ * duplicate the check per call site).
1312
1563
  */
1313
1564
  function taggedHandleId(
1314
1565
  context: string,
1315
1566
  closed: { readonly name: string; readonly handles: readonly string[] },
1316
1567
  value: unknown
1317
1568
  ): TaggedValue {
1318
- if (typeof value !== "bigint") {
1319
- throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, value)
1569
+ if (typeof value !== "string") {
1570
+ throw literalShapeError(context, `a ${closed.name} handle name (string)`, value)
1320
1571
  }
1321
- if (closed.handles[Number(value)] === undefined) {
1572
+ const id = closed.handles.indexOf(value)
1573
+ if (id < 0) {
1322
1574
  throw errors.new(
1323
- `${context}: closed relation ${closed.name} has no handle with id ${value} (roster holds ${closed.handles.length})`
1575
+ `${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
1324
1576
  )
1325
1577
  }
1326
- return { kind: "u64", value }
1578
+ return { kind: "u64", value: BigInt(id) }
1327
1579
  }
1328
1580
 
1329
1581
  /**
@@ -1338,7 +1590,7 @@ function taggedAtElementDomain(context: string, element: "u64" | "i64", value: u
1338
1590
  }
1339
1591
  return { kind: "i64", value }
1340
1592
  }
1341
- if (isIntervalShaped(value)) {
1593
+ if (isIntervalValue(value)) {
1342
1594
  if (element === "u64") {
1343
1595
  return { kind: "intervalU64", start: value.start, end: value.end }
1344
1596
  }
@@ -1356,8 +1608,9 @@ function taggedAtElementDomain(context: string, element: "u64" | "i64", value: u
1356
1608
  * tagged u64 after a roster verification.
1357
1609
  */
1358
1610
  function taggedLiteral(context: string, field: AnyField, value: unknown): TaggedValue {
1359
- if ("closed" in field) {
1360
- return taggedHandleId(context, field.closed, value)
1611
+ const roster = rosterOf(field)
1612
+ if (roster !== undefined) {
1613
+ return taggedHandleId(context, roster, value)
1361
1614
  }
1362
1615
  switch (field.kind) {
1363
1616
  case "bool": {
@@ -1382,6 +1635,18 @@ function taggedLiteral(context: string, field: AnyField, value: unknown): Tagged
1382
1635
  if (typeof value !== "string") {
1383
1636
  throw literalShapeError(context, "string", value)
1384
1637
  }
1638
+ /**
1639
+ * The marshal's bijection law at the query seam (`marshal.ts`
1640
+ * cellOf): a lone surrogate would be lossily replaced with
1641
+ * U+FFFD at the bridge's UTF-8 crossing and silently match a
1642
+ * fact the typed write surface can never store — distinct JS
1643
+ * strings collapsing to one wire query. This is the single
1644
+ * seam every query string literal, string param
1645
+ * (`taggedCmpLiteral`), and membership member lowers through.
1646
+ */
1647
+ if (!value.isWellFormed()) {
1648
+ throw literalShapeError(context, "well-formed string", value)
1649
+ }
1385
1650
  return { kind: "string", value }
1386
1651
  }
1387
1652
  case "bytes": {
@@ -1399,11 +1664,11 @@ function taggedLiteral(context: string, field: AnyField, value: unknown): Tagged
1399
1664
  * Tags one host literal at a COMPARISON or PARAM position, where the
1400
1665
  * SIBLING anchors the type: a measure sibling is u64, an interval-field
1401
1666
  * sibling contributes its element domain (so both a point literal in
1402
- * `covers` and a `span` literal in `allen` tag correctly), a scalar
1667
+ * `pointIn` and a `span` literal in `allen` tag correctly), a scalar
1403
1668
  * sibling its own type. At `pointIn` the operand order is interval-left,
1404
1669
  * point-right (`ir::CmpOp::PointIn`), so an interval-shaped literal
1405
- * beside a scalar element-typed sibling is the LEGAL lhs of
1406
- * `covers(span(...), t)` and tags as the interval of the sibling's
1670
+ * beside a scalar element-typed sibling is the LEGAL interval operand of
1671
+ * `pointIn(t, span(...))` and tags as the interval of the sibling's
1407
1672
  * element domain; under every other operator an interval shape against a
1408
1673
  * scalar sibling stays refused (the engine's IllegalComparison — the
1409
1674
  * bug-hunt fix, preserved op-aware).
@@ -1420,14 +1685,14 @@ function taggedCmpLiteral(
1420
1685
  }
1421
1686
  return { kind: "u64", value }
1422
1687
  }
1423
- if (!("closed" in sibling) && sibling.kind === "interval") {
1688
+ if (rosterOf(sibling) === undefined && sibling.kind === "interval") {
1424
1689
  return taggedAtElementDomain(context, sibling.element, value)
1425
1690
  }
1426
1691
  if (
1427
1692
  op === "pointIn" &&
1428
- !("closed" in sibling) &&
1693
+ rosterOf(sibling) === undefined &&
1429
1694
  (sibling.kind === "u64" || sibling.kind === "i64") &&
1430
- isIntervalShaped(value)
1695
+ isIntervalValue(value)
1431
1696
  ) {
1432
1697
  return taggedAtElementDomain(context, sibling.kind, value)
1433
1698
  }
@@ -1477,7 +1742,7 @@ function paramIdOf(ctx: LowerContext, name: string): number {
1477
1742
  * Lowers one EDB atom (either polarity). A CLOSED owner lowers through the
1478
1743
  * same edb source — its ordinal is its record-declaration slot exactly like
1479
1744
  * an ordinary relation's — with field ordinals over the SEALED shape: `id`
1480
- * at 0, each payload column at its declared index + 1 (`matchFieldsOf`
1745
+ * at 0, each payload column at its declared index + 1 (`sealedFieldsOf`
1481
1746
  * carries the shift; the lowering golden pins it).
1482
1747
  */
1483
1748
  function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
@@ -1491,7 +1756,7 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1491
1756
  if (relationId === undefined) {
1492
1757
  throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`)
1493
1758
  }
1494
- const ordered = matchFieldsOf(atom.relation)
1759
+ const ordered = sealedFieldsOf(atom.relation)
1495
1760
  const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
1496
1761
  const ordinal = ordered.findIndex(function byName(candidate) {
1497
1762
  return candidate.name === binding.field
@@ -1504,7 +1769,12 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1504
1769
  return { source: { kind: "edb", relation: relationId }, bindings }
1505
1770
  }
1506
1771
 
1507
- /** Lowers one binding term. */
1772
+ /**
1773
+ * Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
1774
+ * existing param-set term over its content-addressed registry entry — the
1775
+ * program IR is byte-identical to the same set spelled `r.inSet`; the SDK
1776
+ * supplies the translated member set itself at execute (`wireParams`).
1777
+ */
1508
1778
  function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEntry, ids: VarIds): TermIr {
1509
1779
  const bound = binding.term
1510
1780
  switch (bound.kind) {
@@ -1514,6 +1784,8 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
1514
1784
  return { kind: "param", param: paramIdOf(ctx, bound.name) }
1515
1785
  case "setParam":
1516
1786
  return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
1787
+ case "literalSet":
1788
+ return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
1517
1789
  case "literal":
1518
1790
  return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
1519
1791
  }