@bjornpagen/bumbledb 1.0.1 → 1.2.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 (172) hide show
  1. package/COOKBOOK.md +176 -36
  2. package/README.md +70 -26
  3. package/dist/capacity.d.ts +15 -55
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +63 -12
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +28 -6
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +119 -71
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +8 -14
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +113 -188
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +17 -18
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +41 -20
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +76 -41
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +61 -26
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +152 -85
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +16 -12
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +6 -4
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +3 -1
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +11 -11
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/migration.d.ts +2 -3
  63. package/dist/migration.d.ts.map +1 -1
  64. package/dist/migration.js +5 -5
  65. package/dist/migration.js.map +1 -1
  66. package/dist/native.d.ts +2 -2
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js.map +1 -1
  69. package/dist/query/atom.d.ts +1 -1
  70. package/dist/query/atom.d.ts.map +1 -1
  71. package/dist/query/atom.js.map +1 -1
  72. package/dist/query/description.d.ts +33 -0
  73. package/dist/query/description.d.ts.map +1 -0
  74. package/dist/query/description.js +302 -0
  75. package/dist/query/description.js.map +1 -0
  76. package/dist/query/lower.d.ts +60 -4
  77. package/dist/query/lower.d.ts.map +1 -1
  78. package/dist/query/lower.js +126 -161
  79. package/dist/query/lower.js.map +1 -1
  80. package/dist/query/parse-ir.d.ts +7 -2
  81. package/dist/query/parse-ir.d.ts.map +1 -1
  82. package/dist/query/parse-ir.js +240 -53
  83. package/dist/query/parse-ir.js.map +1 -1
  84. package/dist/query/run.d.ts.map +1 -1
  85. package/dist/query/run.js +8 -10
  86. package/dist/query/run.js.map +1 -1
  87. package/dist/query/scope.d.ts +3 -4
  88. package/dist/query/scope.d.ts.map +1 -1
  89. package/dist/query/scope.js +5 -10
  90. package/dist/query/scope.js.map +1 -1
  91. package/dist/relation.d.ts +22 -48
  92. package/dist/relation.d.ts.map +1 -1
  93. package/dist/relation.js +30 -82
  94. package/dist/relation.js.map +1 -1
  95. package/dist/result.d.ts +10 -25
  96. package/dist/result.d.ts.map +1 -1
  97. package/dist/result.js +7 -12
  98. package/dist/result.js.map +1 -1
  99. package/dist/rows.d.ts +11 -14
  100. package/dist/rows.d.ts.map +1 -1
  101. package/dist/rows.js +49 -237
  102. package/dist/rows.js.map +1 -1
  103. package/dist/runtime-codes.d.ts +1 -1
  104. package/dist/runtime-codes.d.ts.map +1 -1
  105. package/dist/runtime-codes.js +1 -2
  106. package/dist/runtime-codes.js.map +1 -1
  107. package/dist/runtime-errors.d.ts +27 -15
  108. package/dist/runtime-errors.d.ts.map +1 -1
  109. package/dist/runtime-errors.js +44 -8
  110. package/dist/runtime-errors.js.map +1 -1
  111. package/dist/runtime-native.d.ts +16 -34
  112. package/dist/runtime-native.d.ts.map +1 -1
  113. package/dist/runtime-native.js.map +1 -1
  114. package/dist/runtime.d.ts +11 -28
  115. package/dist/runtime.d.ts.map +1 -1
  116. package/dist/runtime.js +14 -54
  117. package/dist/runtime.js.map +1 -1
  118. package/dist/schema.d.ts +9 -1
  119. package/dist/schema.d.ts.map +1 -1
  120. package/dist/schema.js +85 -24
  121. package/dist/schema.js.map +1 -1
  122. package/dist/selection.d.ts +27 -0
  123. package/dist/selection.d.ts.map +1 -0
  124. package/dist/selection.js +104 -0
  125. package/dist/selection.js.map +1 -0
  126. package/dist/shape.d.ts +6 -9
  127. package/dist/shape.d.ts.map +1 -1
  128. package/dist/statements.d.ts +17 -52
  129. package/dist/statements.d.ts.map +1 -1
  130. package/dist/statements.js +92 -111
  131. package/dist/statements.js.map +1 -1
  132. package/dist/values.d.ts +25 -0
  133. package/dist/values.d.ts.map +1 -0
  134. package/dist/values.js +203 -0
  135. package/dist/values.js.map +1 -0
  136. package/pack-provenance.json +3 -3
  137. package/package.json +4 -4
  138. package/src/capacity.ts +86 -76
  139. package/src/changes.ts +225 -94
  140. package/src/closed.ts +161 -314
  141. package/src/codec.ts +123 -213
  142. package/src/compile.ts +61 -38
  143. package/src/db-native.ts +73 -68
  144. package/src/db.ts +268 -165
  145. package/src/errors.ts +16 -1
  146. package/src/face.ts +44 -74
  147. package/src/fields.ts +100 -146
  148. package/src/immutable.ts +76 -0
  149. package/src/index.ts +38 -31
  150. package/src/internal/log.ts +4 -5
  151. package/src/law.ts +14 -14
  152. package/src/lower.ts +13 -10
  153. package/src/migration.ts +5 -12
  154. package/src/native.ts +2 -1
  155. package/src/query/atom.ts +1 -0
  156. package/src/query/description.ts +394 -0
  157. package/src/query/lower.ts +139 -173
  158. package/src/query/parse-ir.ts +247 -54
  159. package/src/query/run.ts +15 -9
  160. package/src/query/scope.ts +10 -18
  161. package/src/relation.ts +58 -142
  162. package/src/result.ts +16 -45
  163. package/src/rows.ts +57 -250
  164. package/src/runtime-codes.ts +1 -2
  165. package/src/runtime-errors.ts +48 -8
  166. package/src/runtime-native.ts +13 -46
  167. package/src/runtime.ts +51 -119
  168. package/src/schema.ts +99 -29
  169. package/src/selection.ts +124 -0
  170. package/src/shape.ts +9 -9
  171. package/src/statements.ts +138 -189
  172. package/src/values.ts +223 -0
@@ -1,15 +1,18 @@
1
- import { sealedFieldsOf } from "#closed.ts"
1
+ import { membersAgree, sealedFieldsOf } from "#closed.ts"
2
2
  import { AuthoringError, SdkInvariantError } from "#errors.ts"
3
3
  import type { AnyClosedRoster, AnyField } from "#fields.ts"
4
4
  import {
5
5
  assertDeclarationOrderKey,
6
6
  f64 as f64Field,
7
+ i64 as i64Field,
7
8
  isFloatIntervalValue,
8
9
  isIntervalValue,
9
10
  literalShapeError,
10
11
  rosterOf,
12
+ rostersAgree,
11
13
  u64 as u64Field
12
14
  } from "#fields.ts"
15
+ import { snapshotData } from "#immutable.ts"
13
16
  import type { ClassRecordOf, SchemaClasses } from "#law.ts"
14
17
  import type {
15
18
  AtomIr,
@@ -86,7 +89,8 @@ import {
86
89
  } from "#query/scope.ts"
87
90
  import { literalWireOf } from "#scalar.ts"
88
91
  import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
89
- import { Uuid } from "#uuid.ts"
92
+ import { schemaDescriptor, schemasAgree } from "#schema.ts"
93
+ import { arrayValue, recordValue, taggedValueOf } from "#values.ts"
90
94
 
91
95
  type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], MatchOwner>
92
96
 
@@ -481,8 +485,8 @@ interface ResolvedBindings {
481
485
 
482
486
  /**
483
487
  * The MINT slot of a variable, the runtime twin of {@link MintSlotOf}: (i)
484
- * verifies the mint owner is the schema's own member value — a variable
485
- * minted from a foreign relation is refused, naming its label — and (ii)
488
+ * verifies the mint owner agrees with the schema's ordered declaration — a
489
+ * conflicting declaration is refused, naming its label — and (ii)
486
490
  * returns the descriptor it was minted at plus the law-computed class read
487
491
  * off the schema's frozen class map. Because {@link fieldJoins} is an
488
492
  * equality, judging every binding position against this one slot makes all
@@ -495,16 +499,16 @@ function mintSlotOf(context: ChainContext, ref: AnyVar): ClassedField {
495
499
  // the descriptor it projected and the carrier class that survived
496
500
  // the projection (aggregate outputs are bare derived scalars).
497
501
  const column = facade.columns.get(ref.column)
498
- if (column === undefined || (facade.source.schema as AnySchema) !== context.theory) {
502
+ if (column === undefined || !schemasAgree(facade.source.schema as AnySchema, context.theory)) {
499
503
  throw new AuthoringError({
500
504
  message: `the variable ${ref.label} was minted from a query template schema ${context.theory.name} does not own`
501
505
  })
502
506
  }
503
507
  return column.slot ?? { field: ref.field, class: undefined }
504
508
  }
505
- if (context.theory.relations[ref.owner.name] !== ref.owner) {
509
+ if (!membersAgree(context.theory.relations[ref.owner.name], ref.owner)) {
506
510
  throw new AuthoringError({
507
- message: `the variable ${ref.label} was minted from a relation schema ${context.theory.name} does not declare — mint variables with v() from the schema's own relations`
511
+ message: `the variable ${ref.label} was minted from a relation schema ${context.theory.name} does not declare — use an equivalent ordered declaration`
508
512
  })
509
513
  }
510
514
  return { field: ref.field, class: context.classes[ref.owner.name]?.[ref.column] }
@@ -532,7 +536,7 @@ function membershipSet(
532
536
  })
533
537
  }
534
538
  const seen = new Set<string>()
535
- const members = value.map(function memberName(member) {
539
+ const members = arrayValue(context, value, function memberName(context, member) {
536
540
  if (typeof member !== "string") {
537
541
  throw literalShapeError(context, `a ${roster.name} handle name (string)`, member)
538
542
  }
@@ -548,22 +552,30 @@ function membershipSet(
548
552
  return { name: `∈ ${roster.name} ${JSON.stringify(key)}`, members: Object.freeze(members) }
549
553
  }
550
554
 
555
+ function queryRecord(context: string, input: unknown): Readonly<Record<string, unknown>> {
556
+ return recordValue(context, input, typeof input === "object" && input !== null ? Object.keys(input) : [])
557
+ }
558
+
551
559
  function resolveBindings(
552
560
  context: ChainContext,
553
561
  label: string,
554
- relation: MatchOwner,
555
- bindings: Readonly<Record<string, unknown>>,
562
+ input: MatchOwner,
563
+ bindings: unknown,
556
564
  joins: (a: ClassedField, b: ClassedField) => boolean = fieldJoins
557
565
  ): ResolvedBindings {
566
+ const relation = context.theory.relations[input.name]
567
+ if (relation === undefined || !membersAgree(relation, input)) {
568
+ throw new AuthoringError({
569
+ message: `${label}: schema ${context.theory.name} does not declare this ordered relation`
570
+ })
571
+ }
572
+ // Retain the schema-owned declaration, not mutable caller metadata.
558
573
  const entries: BindingEntry[] = []
559
574
  const vars: AnyVar[] = []
560
575
  const uses: ParamUse[] = []
561
576
  const relationClasses = context.classes[relation.name]
562
577
  const ordered = sealedFieldsOf(relation)
563
- for (const [fieldName, value] of Object.entries(bindings)) {
564
- if (value === undefined) {
565
- continue
566
- }
578
+ for (const [fieldName, value] of Object.entries(queryRecord(label, bindings))) {
567
579
  const declared = ordered.find(function byName(candidate) {
568
580
  return candidate.name === fieldName
569
581
  })
@@ -627,7 +639,10 @@ function resolveBindings(
627
639
  })
628
640
  )
629
641
  } else {
630
- bound = Object.freeze({ kind: "literal" as const, value })
642
+ bound = Object.freeze({
643
+ kind: "literal" as const,
644
+ value: ownLiteral(`${label}.${fieldName}`, declared.field, value, "binding")
645
+ })
631
646
  }
632
647
  entries.push(Object.freeze({ field: fieldName, data: declared.field, class: fieldClass, term: bound }))
633
648
  }
@@ -683,9 +698,22 @@ function sideUses(op: CmpKind, side: CmpTermData, sibling: CmpTermData, uses: Pa
683
698
  }
684
699
 
685
700
  function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
701
+ queryRecord("query condition", cond)
686
702
  if (cond.cond === "cmp") {
687
- const lhs = cmpTermDataOf(cond.lhs)
688
- const rhs = cmpTermDataOf(cond.rhs)
703
+ let lhs = cmpTermDataOf(cond.lhs)
704
+ let rhs = cmpTermDataOf(cond.rhs)
705
+ if (lhs.kind === "literal" && rhs.kind === "var") {
706
+ lhs = Object.freeze({
707
+ kind: "literal",
708
+ value: ownLiteral("comparison literal", rhs.ref.field, lhs.value, cond.op)
709
+ })
710
+ }
711
+ if (rhs.kind === "literal" && lhs.kind === "var") {
712
+ rhs = Object.freeze({
713
+ kind: "literal",
714
+ value: ownLiteral("comparison literal", lhs.ref.field, rhs.value, cond.op)
715
+ })
716
+ }
689
717
  sideUses(cond.op, lhs, rhs, uses)
690
718
  sideUses(cond.op, rhs, lhs, uses)
691
719
  if (cond.op === "allen") {
@@ -705,8 +733,8 @@ function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
705
733
  return Object.freeze({ kind: "cmp" as const, op: { kind: cond.op }, lhs, rhs })
706
734
  }
707
735
  if (cond.cond === "tree") {
708
- const children = cond.children.map(function lowerChild(child) {
709
- return condDataOf(child, uses)
736
+ const children = arrayValue("query condition children", cond.children, function lowerChild(_context, child) {
737
+ return condDataOf(child as AnyCond, uses)
710
738
  })
711
739
  return Object.freeze({ kind: "tree" as const, op: cond.op, children: Object.freeze(children) })
712
740
  }
@@ -716,25 +744,22 @@ function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
716
744
  }
717
745
 
718
746
  function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCond): RuleBuildState {
747
+ queryRecord("query condition", cond)
719
748
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
720
749
  throw new AuthoringError({ message: "where() takes a comparison, an and()/or() tree, or a negated atom" })
721
750
  }
722
751
  if (cond.cond === "notInterior") {
723
- const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
724
- Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
725
- return value !== undefined
726
- })
727
- )
728
- return notInteriorAdvance(context, state, cond.name, bindings)
752
+ return notInteriorAdvance(context, state, cond.name, queryRecord("negated interior bindings", cond.bindings))
729
753
  }
730
754
  if (cond.cond === "not") {
731
755
  const relation: MatchOwner = cond.relation
732
- const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
733
- Object.entries(cond.bindings ?? {}).filter(function defined([, value]) {
734
- return value !== undefined
735
- })
756
+ const resolved = resolveBindings(
757
+ context,
758
+ `negated relation ${relation.name}`,
759
+ relation,
760
+ cond.bindings,
761
+ fieldAntiJoins
736
762
  )
737
- const resolved = resolveBindings(context, `negated relation ${relation.name}`, relation, bindings, fieldAntiJoins)
738
763
  return Object.freeze({
739
764
  items: Object.freeze([...state.items, Object.freeze({ kind: "negated" as const, atom: resolved.atom })]),
740
765
  bound: state.bound,
@@ -757,10 +782,7 @@ function advanceInterior(
757
782
  kind: "interior" | "negatedInterior"
758
783
  ): RuleBuildState {
759
784
  const resolved: Array<{ readonly key: string; readonly ref: AnyVar }> = []
760
- for (const [key, value] of Object.entries(bindings)) {
761
- if (value === undefined) {
762
- continue
763
- }
785
+ for (const [key, value] of Object.entries(queryRecord(`interior ${target.name} bindings`, bindings))) {
764
786
  if (!isTerm(value) || value[term] !== "var") {
765
787
  throw new AuthoringError({
766
788
  message: `interior ${target.name}: position ${key} takes a variable — bind literals and params through where()/match()`
@@ -1177,28 +1199,43 @@ interface ImportEntry {
1177
1199
 
1178
1200
  const importEntries = new WeakMap<object, ImportEntry>()
1179
1201
  const importTables = new WeakSet<InteriorData>()
1202
+ const importOrigins = new WeakMap<InteriorData, InteriorData>()
1203
+
1204
+ function snapshotQueryData<A>(input: A): A {
1205
+ return snapshotData(input, (source, snapshot) => {
1206
+ // Detached imports retain their authoring identity for deduplication.
1207
+ if (importTables.has(source as InteriorData)) {
1208
+ const table = snapshot as InteriorData
1209
+ importTables.add(table)
1210
+ importOrigins.set(table, importOrigins.get(source as InteriorData) ?? (source as InteriorData))
1211
+ const inner = importInner.get(source as InteriorData)
1212
+ if (inner !== undefined) importInner.set(table, inner)
1213
+ }
1214
+ })
1215
+ }
1180
1216
 
1181
1217
  function importEntryOf(context: ChainContext, imported: AnyQuery): ImportEntry {
1182
1218
  const cached = importEntries.get(imported)
1183
1219
  if (cached !== undefined) {
1184
- if (imported.schema !== context.theory) {
1220
+ if (!schemasAgree(imported.schema, context.theory)) {
1185
1221
  throw new AuthoringError({
1186
1222
  message: `${contextLabel(context)}: the imported query belongs to schema ${imported.schema.name}, not ${context.theory.name} — templates compose within one schema`
1187
1223
  })
1188
1224
  }
1189
1225
  return cached
1190
1226
  }
1191
- if (imported.schema !== context.theory) {
1227
+ if (!schemasAgree(imported.schema, context.theory)) {
1192
1228
  throw new AuthoringError({
1193
1229
  message: `${contextLabel(context)}: the imported query belongs to schema ${imported.schema.name}, not ${context.theory.name} — templates compose within one schema`
1194
1230
  })
1195
1231
  }
1196
- if (imported.data.kind === "reach") {
1232
+ const data = imported.data
1233
+ if (data.kind === "reach") {
1197
1234
  throw new AuthoringError({
1198
1235
  message: `${contextLabel(context)}: a recursive query is not importable as a relation expression yet — declare the recursion on the consuming query (C05 boundary, recorded)`
1199
1236
  })
1200
1237
  }
1201
- const userParams = imported.data.params.filter(function userSupplied(entry) {
1238
+ const userParams = data.params.filter(function userSupplied(entry) {
1202
1239
  return entry.membership === undefined
1203
1240
  })
1204
1241
  if (userParams.length > 0) {
@@ -1211,10 +1248,10 @@ function importEntryOf(context: ChainContext, imported: AnyQuery): ImportEntry {
1211
1248
  const facade = importFacadeOf(imported)
1212
1249
  const table: InteriorData = Object.freeze({
1213
1250
  name: facade.owner.name,
1214
- finds: imported.data.finds,
1215
- rules: imported.data.rules
1251
+ finds: data.finds,
1252
+ rules: data.rules
1216
1253
  })
1217
- const entry: ImportEntry = Object.freeze({ table, inner: imported.data.interiors })
1254
+ const entry: ImportEntry = Object.freeze({ table, inner: data.interiors })
1218
1255
  importEntries.set(imported, entry)
1219
1256
  importTables.add(table)
1220
1257
  importInner.set(table, entry.inner)
@@ -1242,8 +1279,9 @@ function referencedImports(
1242
1279
  continue
1243
1280
  }
1244
1281
  const target = item.target as InteriorData
1245
- if (importTables.has(target) && !seen.has(target)) {
1246
- seen.add(target)
1282
+ const origin = importOrigins.get(target) ?? target
1283
+ if (importTables.has(target) && !seen.has(origin)) {
1284
+ seen.add(origin)
1247
1285
  out.push(target)
1248
1286
  }
1249
1287
  }
@@ -1280,7 +1318,8 @@ function spliceImports(
1280
1318
  const names = new Set<string>()
1281
1319
  const added = new Set<InteriorData>()
1282
1320
  function add(table: InteriorData): void {
1283
- if (added.has(table)) {
1321
+ const origin = importOrigins.get(table) ?? table
1322
+ if (added.has(origin)) {
1284
1323
  return
1285
1324
  }
1286
1325
  if (names.has(table.name)) {
@@ -1288,7 +1327,7 @@ function spliceImports(
1288
1327
  message: `query: the derived-table name ${table.name} appears twice after import splicing — rename the colliding interior (names are unique)`
1289
1328
  })
1290
1329
  }
1291
- added.add(table)
1330
+ added.add(origin)
1292
1331
  names.add(table.name)
1293
1332
  spliced.push(table)
1294
1333
  }
@@ -1330,15 +1369,13 @@ function notInteriorAdvance(
1330
1369
 
1331
1370
  function findColumns(context: ChainContext, entries: Readonly<Record<string, unknown>>): FindColumn[] {
1332
1371
  const columns: FindColumn[] = []
1372
+ queryRecord("query find", entries)
1333
1373
  // Nonrecursive derived stages (interiors) may emit aggregate/computed
1334
1374
  // outputs (C05: "do not leave Interior.rules projection only"); ONLY the
1335
1375
  // recursive feedback cycle stays projection-only — no aggregate,
1336
1376
  // arithmetic-created value or negation flows through it.
1337
1377
  const recName = context.kind === "rec-base" || context.kind === "rec-arm" ? context.self.name : undefined
1338
1378
  for (const [name, entry] of Object.entries(entries)) {
1339
- if (entry === undefined) {
1340
- continue
1341
- }
1342
1379
  if (recName !== undefined && !(isTerm(entry) && entry[term] === "var")) {
1343
1380
  throw new AuthoringError({
1344
1381
  message: `rec ${recName}: a rec head projects bound variables only — no aggregate or arithmetic-created value flows through the feedback cycle (compute over the finished set in the main rules)`
@@ -1540,7 +1577,7 @@ function paramRegistryOf(
1540
1577
  if (existing.anchor !== undefined && use.anchor !== undefined) {
1541
1578
  const registered = rosterOf(existing.anchor)
1542
1579
  const anchored = rosterOf(use.anchor)
1543
- if (registered !== anchored) {
1580
+ if (!rostersAgree(registered, anchored)) {
1544
1581
  throw new AuthoringError({
1545
1582
  message: `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`
1546
1583
  })
@@ -1627,7 +1664,7 @@ function alignedHeadOf(label: string, rules: readonly RuleData[]): readonly Find
1627
1664
  }
1628
1665
  rule.finds.forEach(function verifyClosedSlice(column, position) {
1629
1666
  const lead = first.finds[position]
1630
- if (lead !== undefined && column.closed !== lead.closed) {
1667
+ if (lead !== undefined && !rostersAgree(column.closed, lead.closed)) {
1631
1668
  throw new AuthoringError({
1632
1669
  message: `every rule of ${label} derives the same head — the head column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`
1633
1670
  })
@@ -1671,11 +1708,17 @@ function afterMainError(what: string): Error {
1671
1708
  }
1672
1709
 
1673
1710
  function makeRawQuery(
1674
- theory: AnySchema,
1675
- interiors: readonly InteriorData[],
1676
- rec: RecData | undefined,
1677
- rules: readonly RuleData[]
1711
+ inputTheory: AnySchema,
1712
+ inputInteriors: readonly InteriorData[],
1713
+ inputRec: RecData | undefined,
1714
+ inputRules: readonly RuleData[]
1678
1715
  ): RawQuery {
1716
+ const { theory, interiors, rec, rules } = snapshotQueryData({
1717
+ theory: inputTheory,
1718
+ interiors: inputInteriors,
1719
+ rec: inputRec,
1720
+ rules: inputRules
1721
+ })
1679
1722
  const mergedFinds = alignedHeadOf("a query", rules)
1680
1723
  const first = rules[0]
1681
1724
  if (first === undefined) {
@@ -1704,8 +1747,12 @@ function makeRawQuery(
1704
1747
  params
1705
1748
  })
1706
1749
  const value: RawQuery = {
1707
- schema: theory,
1708
- data,
1750
+ get schema() {
1751
+ return snapshotData(theory)
1752
+ },
1753
+ get data() {
1754
+ return snapshotQueryData(data)
1755
+ },
1709
1756
  rule(build) {
1710
1757
  const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory, ...env }))
1711
1758
  return makeRawQuery(theory, interiors, rec, [...rules, built.rule])
@@ -1799,9 +1846,9 @@ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>
1799
1846
  const sealedRec: RecData["rec"] = [firstRec, ...rec.slice(1)]
1800
1847
  const recData: RecData = Object.freeze({
1801
1848
  name,
1802
- finds: sealedFinds,
1803
- base: sealedBase,
1804
- rec: sealedRec
1849
+ finds: Object.freeze(sealedFinds),
1850
+ base: Object.freeze(sealedBase),
1851
+ rec: Object.freeze(sealedRec)
1805
1852
  })
1806
1853
  return recData
1807
1854
  }
@@ -1882,112 +1929,21 @@ function makeQueryReachStart<Rels extends SchemaRelations, Classes extends Schem
1882
1929
  function query<Rels extends SchemaRelations, Classes extends SchemaClasses>(
1883
1930
  theory: Schema<Rels, Classes>
1884
1931
  ): QueryStart<Rels, Classes> {
1885
- return makeQueryStart<Rels, Classes, Record<never, never>>(theory, [])
1886
- }
1887
-
1888
- function taggedHandleId(
1889
- context: string,
1890
- closed: { readonly name: string; readonly handles: readonly string[] },
1891
- value: unknown
1892
- ): TaggedValue {
1893
- if (typeof value !== "string") {
1894
- throw literalShapeError(context, `a ${closed.name} handle name (string)`, value)
1895
- }
1896
- const id = closed.handles.indexOf(value)
1897
- if (id < 0) {
1898
- throw new AuthoringError({
1899
- message: `${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
1900
- })
1901
- }
1902
- return { kind: "u64", value: BigInt(id) }
1932
+ return makeQueryStart<Rels, Classes, Record<never, never>>(schemaDescriptor(theory), [])
1903
1933
  }
1904
1934
 
1905
1935
  function taggedAtElementDomain(context: string, element: "u64" | "i64" | "f64", value: unknown): TaggedValue {
1906
- if (element === "f64") {
1907
- if (typeof value === "number") {
1908
- return { kind: "f64", value }
1909
- }
1910
- if (isFloatIntervalValue(value)) {
1911
- if (Number.isNaN(value.start) || Number.isNaN(value.end) || !(value.start < value.end)) {
1912
- throw literalShapeError(context, "a nonempty float interval with non-NaN endpoints", value)
1913
- }
1914
- return {
1915
- kind: "intervalF64",
1916
- start: Object.is(value.start, -0) ? 0 : value.start,
1917
- end: Object.is(value.end, -0) ? 0 : value.end
1918
- }
1919
- }
1920
- throw literalShapeError(context, "number (point) or { start, end } numbers (interval)", value)
1921
- }
1922
- if (typeof value === "bigint") {
1923
- if (element === "u64") {
1924
- return { kind: "u64", value }
1925
- }
1926
- return { kind: "i64", value }
1927
- }
1928
- if (isIntervalValue(value)) {
1929
- if (element === "u64") {
1930
- return { kind: "intervalU64", start: value.start, end: value.end }
1931
- }
1932
- return { kind: "intervalI64", start: value.start, end: value.end }
1933
- }
1934
- throw literalShapeError(context, "bigint (point) or { start, end } (interval)", value)
1936
+ const field: AnyField =
1937
+ typeof value === "number" || typeof value === "bigint"
1938
+ ? { u64: u64Field, i64: i64Field, f64: f64Field }[element]
1939
+ : { kind: "interval", element, width: undefined }
1940
+ return taggedValueOf(context, field, value)
1935
1941
  }
1936
1942
 
1937
1943
  function taggedLiteral(context: string, field: AnyField, value: unknown): TaggedValue {
1938
- const roster = rosterOf(field)
1939
- if (roster !== undefined) {
1940
- return taggedHandleId(context, roster, value)
1941
- }
1942
- switch (field.kind) {
1943
- case "bool": {
1944
- if (typeof value !== "boolean") {
1945
- throw literalShapeError(context, "boolean", value)
1946
- }
1947
- return { kind: "bool", value }
1948
- }
1949
- case "u64": {
1950
- if (typeof value !== "bigint") {
1951
- throw literalShapeError(context, "bigint", value)
1952
- }
1953
- return { kind: "u64", value }
1954
- }
1955
- case "i64": {
1956
- if (typeof value !== "bigint") {
1957
- throw literalShapeError(context, "bigint", value)
1958
- }
1959
- return { kind: "i64", value }
1960
- }
1961
- case "f64": {
1962
- if (typeof value !== "number") {
1963
- throw literalShapeError(context, "number", value)
1964
- }
1965
- return { kind: "f64", value }
1966
- }
1967
- case "uuid": {
1968
- if (!Uuid.isUuid(value)) {
1969
- throw literalShapeError(context, "a UUID (canonical UUID text)", value)
1970
- }
1971
- return { kind: "uuid", value }
1972
- }
1973
- case "str": {
1974
- if (typeof value !== "string") {
1975
- throw literalShapeError(context, "string", value)
1976
- }
1977
- if (!value.isWellFormed()) {
1978
- throw literalShapeError(context, "well-formed string", value)
1979
- }
1980
- return { kind: "string", value }
1981
- }
1982
- case "bytes": {
1983
- if (!(value instanceof Uint8Array)) {
1984
- throw literalShapeError(context, "Uint8Array", value)
1985
- }
1986
- return { kind: "fixedBytes", value }
1987
- }
1988
- case "interval":
1989
- return taggedAtElementDomain(context, field.element, value)
1990
- }
1944
+ return field.kind === "interval"
1945
+ ? taggedAtElementDomain(context, field.element, value)
1946
+ : taggedValueOf(context, field, value)
1991
1947
  }
1992
1948
 
1993
1949
  /**
@@ -1996,7 +1952,7 @@ function taggedLiteral(context: string, field: AnyField, value: unknown): Tagged
1996
1952
  * contributes its element domain, a scalar sibling its own type. At
1997
1953
  * `pointIn` the operand order is interval-left, point-right, so an
1998
1954
  * interval-shaped literal beside a scalar element-typed sibling is the LEGAL
1999
- * interval operand of `pointIn(t, span(...))`; under every other operator an
1955
+ * interval operand of `pointIn(t, { start, end })`; under every other operator an
2000
1956
  * interval shape against a scalar sibling stays refused.
2001
1957
  */
2002
1958
  function taggedCmpLiteral(context: string, sibling: AnyField, value: unknown, op: CmpKind | "binding"): TaggedValue {
@@ -2017,6 +1973,14 @@ function taggedCmpLiteral(context: string, sibling: AnyField, value: unknown, op
2017
1973
  return taggedLiteral(context, sibling, value)
2018
1974
  }
2019
1975
 
1976
+ /** Validate and own a literal when the query is authored, before caller mutation. */
1977
+ function ownLiteral(context: string, field: AnyField, value: unknown, op: CmpKind | "binding"): unknown {
1978
+ const tagged = taggedCmpLiteral(context, field, value, op)
1979
+ if (rosterOf(field) !== undefined) return value
1980
+ if ("value" in tagged) return tagged.value
1981
+ return Object.freeze({ start: tagged.start, end: tagged.end })
1982
+ }
1983
+
2020
1984
  interface LowerContext {
2021
1985
  readonly theory: AnySchema
2022
1986
  readonly relationIds: ReadonlyMap<string, number>
@@ -2054,7 +2018,7 @@ function paramIdOf(ctx: LowerContext, name: string): number {
2054
2018
 
2055
2019
  function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
2056
2020
  const member = ctx.theory.relations[atom.relation.name]
2057
- if (member !== atom.relation) {
2021
+ if (!membersAgree(member, atom.relation)) {
2058
2022
  throw new AuthoringError({
2059
2023
  message: `query lowering: relation ${atom.relation.name} is not the relation value schema ${ctx.theory.name} declares`
2060
2024
  })
@@ -2306,20 +2270,21 @@ function lowerRule(ctx: LowerContext, rule: RuleData): RuleIr {
2306
2270
 
2307
2271
  function lowerQuery(q: AnyQuery): ParsedQuery {
2308
2272
  const theory = q.schema
2273
+ const data = q.data
2309
2274
  const relationIds = new Map<string, number>()
2310
2275
  Object.keys(theory.relations).forEach(function assignOrdinal(name, index) {
2311
2276
  relationIds.set(name, index)
2312
2277
  })
2313
2278
  const interiorIds = new Map<string, number>()
2314
- q.data.interiors.forEach(function assignInteriorId(interior, index) {
2279
+ data.interiors.forEach(function assignInteriorId(interior, index) {
2315
2280
  interiorIds.set(interior.name, index)
2316
2281
  })
2317
- if (q.data.kind === "reach") {
2318
- interiorIds.set(q.data.rec.name, q.data.interiors.length)
2282
+ if (data.kind === "reach") {
2283
+ interiorIds.set(data.rec.name, data.interiors.length)
2319
2284
  }
2320
2285
  const paramIds = new Map<string, number>()
2321
2286
  const params = new Map<string, ParamEntry>()
2322
- q.data.params.forEach(function assignParamId(entry, index) {
2287
+ data.params.forEach(function assignParamId(entry, index) {
2323
2288
  if (entry.anchor === undefined) {
2324
2289
  throw new AuthoringError({
2325
2290
  message: `query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`
@@ -2329,7 +2294,7 @@ function lowerQuery(q: AnyQuery): ParsedQuery {
2329
2294
  params.set(entry.name, entry)
2330
2295
  })
2331
2296
  const ctx: LowerContext = { theory, relationIds, interiorIds, paramIds, params }
2332
- const interiors = q.data.interiors.map(function lowerInterior(interior) {
2297
+ const interiors = data.interiors.map(function lowerInterior(interior) {
2333
2298
  return {
2334
2299
  head: interior.finds.map(headTermOf),
2335
2300
  rules: interior.rules.map(function lowerInteriorRule(rule) {
@@ -2337,22 +2302,22 @@ function lowerQuery(q: AnyQuery): ParsedQuery {
2337
2302
  })
2338
2303
  }
2339
2304
  })
2340
- const head = q.data.finds.map(headTermOf)
2341
- const rules = q.data.rules.map(function lowerMainRule(rule) {
2305
+ const head = data.finds.map(headTermOf)
2306
+ const rules = data.rules.map(function lowerMainRule(rule) {
2342
2307
  return lowerRule(ctx, rule)
2343
2308
  })
2344
- if (q.data.kind === "cq") {
2309
+ if (data.kind === "cq") {
2345
2310
  return parseQueryIr({ kind: "cq", interiors, head, rules })
2346
2311
  }
2347
2312
  return parseQueryIr({
2348
2313
  kind: "reach",
2349
2314
  interiors,
2350
2315
  rec: {
2351
- head: q.data.rec.finds.map(headTermOf),
2352
- base: q.data.rec.base.map(function lowerBase(rule) {
2316
+ head: data.rec.finds.map(headTermOf),
2317
+ base: data.rec.base.map(function lowerBase(rule) {
2353
2318
  return lowerRule(ctx, rule)
2354
2319
  }),
2355
- rec: q.data.rec.rec.map(function lowerRecArm(rule) {
2320
+ rec: data.rec.rec.map(function lowerRecArm(rule) {
2356
2321
  return lowerRule(ctx, rule)
2357
2322
  })
2358
2323
  },
@@ -2364,6 +2329,7 @@ function lowerQuery(q: AnyQuery): ParsedQuery {
2364
2329
  export type {
2365
2330
  AnyQuery,
2366
2331
  AnyRuleValue,
2332
+ ChainContext,
2367
2333
  Query,
2368
2334
  QueryData,
2369
2335
  QueryParams,
@@ -2378,4 +2344,4 @@ export type {
2378
2344
  RuleValue,
2379
2345
  TermOps
2380
2346
  }
2381
- export { lowerQuery, query, taggedCmpLiteral }
2347
+ export { alignedHeadOf, EMPTY_RULE, lowerQuery, makeRawChain, makeRawQuery, query, taggedCmpLiteral }