@bjornpagen/bumbledb 0.20.2 → 1.0.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 (192) hide show
  1. package/COOKBOOK.md +283 -1611
  2. package/README.md +156 -118
  3. package/dist/capacity.d.ts +16 -12
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +16 -48
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +54 -0
  8. package/dist/changes.d.ts.map +1 -0
  9. package/dist/changes.js +245 -0
  10. package/dist/changes.js.map +1 -0
  11. package/dist/close.d.ts +24 -0
  12. package/dist/close.d.ts.map +1 -0
  13. package/dist/close.js +34 -0
  14. package/dist/close.js.map +1 -0
  15. package/dist/closed.d.ts +1 -3
  16. package/dist/closed.d.ts.map +1 -1
  17. package/dist/closed.js +24 -12
  18. package/dist/closed.js.map +1 -1
  19. package/dist/codec.d.ts +56 -0
  20. package/dist/codec.d.ts.map +1 -0
  21. package/dist/codec.js +289 -0
  22. package/dist/codec.js.map +1 -0
  23. package/dist/compile.d.ts +68 -0
  24. package/dist/compile.d.ts.map +1 -0
  25. package/dist/compile.js +75 -0
  26. package/dist/compile.js.map +1 -0
  27. package/dist/db-native.d.ts +170 -0
  28. package/dist/db-native.d.ts.map +1 -0
  29. package/dist/db-native.js +5 -0
  30. package/dist/db-native.js.map +1 -0
  31. package/dist/db.d.ts +101 -233
  32. package/dist/db.d.ts.map +1 -1
  33. package/dist/db.js +232 -1084
  34. package/dist/db.js.map +1 -1
  35. package/dist/errors.d.ts +57 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +32 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/face.d.ts.map +1 -1
  40. package/dist/face.js +2 -2
  41. package/dist/face.js.map +1 -1
  42. package/dist/fields.d.ts +65 -24
  43. package/dist/fields.d.ts.map +1 -1
  44. package/dist/fields.js +94 -24
  45. package/dist/fields.js.map +1 -1
  46. package/dist/index.d.ts +37 -32
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +17 -21
  49. package/dist/index.js.map +1 -1
  50. package/dist/internal/log.d.ts +24 -0
  51. package/dist/internal/log.d.ts.map +1 -0
  52. package/dist/internal/log.js +10 -0
  53. package/dist/internal/log.js.map +1 -0
  54. package/dist/law.d.ts +8 -13
  55. package/dist/law.d.ts.map +1 -1
  56. package/dist/law.js +6 -59
  57. package/dist/law.js.map +1 -1
  58. package/dist/lower.d.ts +0 -13
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +9 -17
  61. package/dist/lower.js.map +1 -1
  62. package/dist/migration.d.ts +33 -0
  63. package/dist/migration.d.ts.map +1 -0
  64. package/dist/migration.js +48 -0
  65. package/dist/migration.js.map +1 -0
  66. package/dist/native.d.ts +256 -301
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js +65 -108
  69. package/dist/native.js.map +1 -1
  70. package/dist/query/atom.d.ts +24 -13
  71. package/dist/query/atom.d.ts.map +1 -1
  72. package/dist/query/atom.js +7 -3
  73. package/dist/query/atom.js.map +1 -1
  74. package/dist/query/compute.d.ts +68 -0
  75. package/dist/query/compute.d.ts.map +1 -0
  76. package/dist/query/compute.js +141 -0
  77. package/dist/query/compute.js.map +1 -0
  78. package/dist/query/find.d.ts +15 -8
  79. package/dist/query/find.d.ts.map +1 -1
  80. package/dist/query/find.js +6 -2
  81. package/dist/query/find.js.map +1 -1
  82. package/dist/query/lower.d.ts +23 -2
  83. package/dist/query/lower.d.ts.map +1 -1
  84. package/dist/query/lower.js +433 -80
  85. package/dist/query/lower.js.map +1 -1
  86. package/dist/query/parse-ir.d.ts.map +1 -1
  87. package/dist/query/parse-ir.js +17 -8
  88. package/dist/query/parse-ir.js.map +1 -1
  89. package/dist/query/run.d.ts +12 -2
  90. package/dist/query/run.d.ts.map +1 -1
  91. package/dist/query/run.js +26 -9
  92. package/dist/query/run.js.map +1 -1
  93. package/dist/query/scope.d.ts +47 -3
  94. package/dist/query/scope.d.ts.map +1 -1
  95. package/dist/query/scope.js +83 -19
  96. package/dist/query/scope.js.map +1 -1
  97. package/dist/relation.d.ts +3 -8
  98. package/dist/relation.d.ts.map +1 -1
  99. package/dist/relation.js +16 -10
  100. package/dist/relation.js.map +1 -1
  101. package/dist/result.d.ts +47 -0
  102. package/dist/result.d.ts.map +1 -0
  103. package/dist/result.js +50 -0
  104. package/dist/result.js.map +1 -0
  105. package/dist/rows.d.ts +69 -0
  106. package/dist/rows.d.ts.map +1 -0
  107. package/dist/rows.js +328 -0
  108. package/dist/rows.js.map +1 -0
  109. package/dist/runtime-codes.d.ts +2 -0
  110. package/dist/runtime-codes.d.ts.map +1 -0
  111. package/dist/runtime-codes.js +19 -0
  112. package/dist/runtime-codes.js.map +1 -0
  113. package/dist/runtime-errors.d.ts +106 -0
  114. package/dist/runtime-errors.d.ts.map +1 -0
  115. package/dist/runtime-errors.js +62 -0
  116. package/dist/runtime-errors.js.map +1 -0
  117. package/dist/runtime-native.d.ts +171 -0
  118. package/dist/runtime-native.d.ts.map +1 -0
  119. package/dist/runtime-native.js +5 -0
  120. package/dist/runtime-native.js.map +1 -0
  121. package/dist/runtime.d.ts +73 -0
  122. package/dist/runtime.d.ts.map +1 -0
  123. package/dist/runtime.js +293 -0
  124. package/dist/runtime.js.map +1 -0
  125. package/dist/scalar.d.ts +165 -0
  126. package/dist/scalar.d.ts.map +1 -0
  127. package/dist/scalar.js +344 -0
  128. package/dist/scalar.js.map +1 -0
  129. package/dist/schema.d.ts +1 -1
  130. package/dist/schema.d.ts.map +1 -1
  131. package/dist/schema.js +34 -37
  132. package/dist/schema.js.map +1 -1
  133. package/dist/shape.d.ts +28 -0
  134. package/dist/shape.d.ts.map +1 -0
  135. package/dist/shape.js +2 -0
  136. package/dist/shape.js.map +1 -0
  137. package/dist/spec.d.ts +27 -3
  138. package/dist/spec.d.ts.map +1 -1
  139. package/dist/spec.js +22 -1
  140. package/dist/spec.js.map +1 -1
  141. package/dist/statements.d.ts +16 -3
  142. package/dist/statements.d.ts.map +1 -1
  143. package/dist/statements.js +54 -28
  144. package/dist/statements.js.map +1 -1
  145. package/dist/uuid.d.ts +23 -0
  146. package/dist/uuid.d.ts.map +1 -0
  147. package/dist/uuid.js +54 -0
  148. package/dist/uuid.js.map +1 -0
  149. package/pack-provenance.json +7 -0
  150. package/package.json +12 -16
  151. package/src/capacity.ts +34 -54
  152. package/src/changes.ts +361 -0
  153. package/src/close.ts +53 -0
  154. package/src/closed.ts +25 -19
  155. package/src/codec.ts +379 -0
  156. package/src/compile.ts +149 -0
  157. package/src/db-native.ts +247 -0
  158. package/src/db.ts +461 -1632
  159. package/src/errors.ts +50 -0
  160. package/src/face.ts +2 -2
  161. package/src/fields.ts +182 -55
  162. package/src/index.ts +62 -126
  163. package/src/internal/log.ts +51 -0
  164. package/src/law.ts +13 -28
  165. package/src/lower.ts +9 -4
  166. package/src/migration.ts +69 -0
  167. package/src/native.ts +326 -487
  168. package/src/query/atom.ts +61 -25
  169. package/src/query/compute.ts +237 -0
  170. package/src/query/find.ts +31 -12
  171. package/src/query/lower.ts +531 -143
  172. package/src/query/parse-ir.ts +18 -13
  173. package/src/query/run.ts +32 -16
  174. package/src/query/scope.ts +140 -8
  175. package/src/relation.ts +17 -21
  176. package/src/result.ts +130 -0
  177. package/src/rows.ts +404 -0
  178. package/src/runtime-codes.ts +18 -0
  179. package/src/runtime-errors.ts +69 -0
  180. package/src/runtime-native.ts +242 -0
  181. package/src/runtime.ts +440 -0
  182. package/src/scalar.ts +560 -0
  183. package/src/schema.ts +35 -37
  184. package/src/shape.ts +31 -0
  185. package/src/spec.ts +33 -3
  186. package/src/statements.ts +75 -68
  187. package/src/uuid.ts +69 -0
  188. package/dist/marshal.d.ts +0 -44
  189. package/dist/marshal.d.ts.map +0 -1
  190. package/dist/marshal.js +0 -165
  191. package/dist/marshal.js.map +0 -1
  192. package/src/marshal.ts +0 -200
@@ -1,7 +1,15 @@
1
- import * as errors from "@superbuilders/errors"
2
1
  import { sealedFieldsOf } from "#closed.ts"
2
+ import { AuthoringError, SdkInvariantError } from "#errors.ts"
3
3
  import type { AnyClosedRoster, AnyField } from "#fields.ts"
4
- import { assertDeclarationOrderKey, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
4
+ import {
5
+ assertDeclarationOrderKey,
6
+ f64 as f64Field,
7
+ isFloatIntervalValue,
8
+ isIntervalValue,
9
+ literalShapeError,
10
+ rosterOf,
11
+ u64 as u64Field
12
+ } from "#fields.ts"
5
13
  import type { ClassRecordOf, SchemaClasses } from "#law.ts"
6
14
  import type {
7
15
  AtomIr,
@@ -13,6 +21,7 @@ import type {
13
21
  ParsedQuery,
14
22
  QueryParam,
15
23
  RuleIr,
24
+ ScalarExprIr,
16
25
  TaggedValue,
17
26
  TermIr
18
27
  } from "#native.ts"
@@ -44,8 +53,10 @@ import type {
44
53
  RuleItem
45
54
  } from "#query/atom.ts"
46
55
  import { allen, and, eq, ge, gt, le, lt, ne, not, or, pointIn } from "#query/atom.ts"
56
+ import type { QueryNode } from "#query/compute.ts"
57
+ import { computeFieldOf, computeVarsOf, isComputeExpr, MAX_COMPUTE_DEPTH } from "#query/compute.ts"
47
58
  import type { CheckFind, CheckRecFind, FindShape, HeadRecordOf, RowOfFind } from "#query/find.ts"
48
- import { count, max, min, pack, sum } from "#query/find.ts"
59
+ import { count, max, mean, min, pack, sum } from "#query/find.ts"
49
60
  import { parseQueryIr } from "#query/parse-ir.ts"
50
61
  import type {
51
62
  AnyVar,
@@ -62,14 +73,20 @@ import {
62
73
  fieldAntiJoins,
63
74
  fieldJoins,
64
75
  headFieldJoins,
76
+ importFacadeOf,
77
+ importFacadeOfOwner,
65
78
  inferred,
79
+ isImportedSource,
66
80
  isTerm,
81
+ labelImport,
67
82
  makeParam,
68
83
  makeSetParam,
69
84
  renderFieldKind,
70
85
  term
71
86
  } from "#query/scope.ts"
87
+ import { literalWireOf } from "#scalar.ts"
72
88
  import type { AnySchema, Schema, SchemaRelations } from "#schema.ts"
89
+ import { Uuid } from "#uuid.ts"
73
90
 
74
91
  type QueryRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], MatchOwner>
75
92
 
@@ -121,6 +138,7 @@ interface TermOps {
121
138
  readonly not: typeof not
122
139
  readonly count: typeof count
123
140
  readonly sum: typeof sum
141
+ readonly mean: typeof mean
124
142
  readonly min: typeof min
125
143
  readonly max: typeof max
126
144
  readonly pack: typeof pack
@@ -147,12 +165,25 @@ interface QueryRuleScope<Rels extends SchemaRelations, Classes extends SchemaCla
147
165
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
148
166
  ): QueryRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
149
167
 
168
+ /**
169
+ * Nonrecursive composition (chapter 34): a typed query template of the
170
+ * SAME schema is a relation expression — its whole body splices as a
171
+ * derived stage; every head column must be bound to a variable minted
172
+ * by `v(imported)`. Naming materializes nothing.
173
+ */
174
+ match<Q extends AnyQuery, const B extends ImportMatchShape<Q>>(
175
+ imported: Q,
176
+ bindings: B
177
+ ): QueryRuleChain<Rels, Record<never, never>, Classes>
178
+
150
179
  interior<const B extends Readonly<Record<string, AnyVar>>>(
151
180
  name: string,
152
181
  bindings: B & CheckInteriorBindings<B>
153
182
  ): QueryRuleChain<Rels, Record<never, never>, Classes>
154
183
  }
155
184
 
185
+ type ImportMatchShape<Q extends AnyQuery> = Readonly<Record<keyof RowOf<Q> & string, AnyVar>>
186
+
156
187
  interface QueryRuleChain<
157
188
  Rels extends SchemaRelations,
158
189
  P extends ParamsRecord,
@@ -176,6 +207,11 @@ interface QueryRuleChain<
176
207
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
177
208
  ): QueryRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
178
209
 
210
+ match<Q extends AnyQuery, const B extends ImportMatchShape<Q>>(
211
+ imported: Q,
212
+ bindings: B
213
+ ): QueryRuleChain<Rels, P, Classes>
214
+
179
215
  where<const C extends AnyCond>(
180
216
  cond: CheckCond<Classes, C> & C
181
217
  ): QueryRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
@@ -206,6 +242,10 @@ interface InteriorRuleScope<Rels extends SchemaRelations, Classes extends Schema
206
242
  relation: R,
207
243
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
208
244
  ): InteriorRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
245
+ match<Q extends AnyQuery, const B extends ImportMatchShape<Q>>(
246
+ imported: Q,
247
+ bindings: B
248
+ ): InteriorRuleChain<Rels, Record<never, never>, Classes>
209
249
  interior<const B extends Readonly<Record<string, AnyVar>>>(
210
250
  name: string,
211
251
  bindings: B & CheckInteriorBindings<B>
@@ -233,6 +273,10 @@ interface InteriorRuleChain<
233
273
  relation: R,
234
274
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
235
275
  ): InteriorRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
276
+ match<Q extends AnyQuery, const B extends ImportMatchShape<Q>>(
277
+ imported: Q,
278
+ bindings: B
279
+ ): InteriorRuleChain<Rels, P, Classes>
236
280
  where<const C extends AnyCond>(
237
281
  cond: CheckCond<Classes, C> & C
238
282
  ): InteriorRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
@@ -240,7 +284,11 @@ interface InteriorRuleChain<
240
284
  name: string,
241
285
  bindings: B & CheckInteriorBindings<B>
242
286
  ): InteriorRuleChain<Rels, P, Classes>
243
- find<const F extends FindShape>(entries: F & CheckRecFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
287
+ /**
288
+ * Nonrecursive derived stages emit aggregate/computed outputs too
289
+ * (C05): only the RECURSIVE head stays projection-only.
290
+ */
291
+ find<const F extends FindShape>(entries: F & CheckFind<F>): RuleValue<RowOfFind<F>, P, HeadRecordOf<Classes, F>>
244
292
  }
245
293
 
246
294
  interface RecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> extends TermOps {
@@ -260,6 +308,11 @@ interface RecRuleScope<Rels extends SchemaRelations, Classes extends SchemaClass
260
308
  relation: R,
261
309
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
262
310
  ): RecRuleChain<Rels, BindParamsShape<MatchFields<R>, B>, Classes>
311
+ /** Frozen finite nonrecursive imports may feed base/step (chapter 34). */
312
+ match<Q extends AnyQuery, const B extends ImportMatchShape<Q>>(
313
+ imported: Q,
314
+ bindings: B
315
+ ): RecRuleChain<Rels, Record<never, never>, Classes>
263
316
  interior<const B extends Readonly<Record<string, AnyVar>>>(
264
317
  name: string,
265
318
  bindings: B & CheckInteriorBindings<B>
@@ -287,6 +340,10 @@ interface RecRuleChain<
287
340
  relation: R,
288
341
  bindings: B & CheckBindings<Classes, MatchFields<R>, ClassRecordOf<Classes, R["name"]>, B>
289
342
  ): RecRuleChain<Rels, Flatten<P & BindParamsShape<MatchFields<R>, B>>, Classes>
343
+ match<Q extends AnyQuery, const B extends ImportMatchShape<Q>>(
344
+ imported: Q,
345
+ bindings: B
346
+ ): RecRuleChain<Rels, P, Classes>
290
347
  where<const C extends AnyCond>(
291
348
  cond: CheckCond<Classes, C> & C
292
349
  ): RecRuleChain<Rels, Flatten<P & CondParamsShape<C>>, Classes>
@@ -336,6 +393,9 @@ interface Query<
336
393
  build: (r: QueryRuleScope<Rels, Classes>) => RV
337
394
  ): Query<Rels, Row | RowOf<RV>, Flatten<Params & ParamsOf<RV>>, Classes>
338
395
 
396
+ /** A diagnostic name for composition/tracing — never a schema relation. */
397
+ named(label: string): Query<Rels, Row, Params, Classes>
398
+
339
399
  interior(name: string, ...builds: never[]): never
340
400
 
341
401
  reach(name: string, arms: never): never
@@ -395,6 +455,7 @@ const termOps: TermOps = Object.freeze({
395
455
  not,
396
456
  count,
397
457
  sum,
458
+ mean,
398
459
  min,
399
460
  max,
400
461
  pack
@@ -428,10 +489,23 @@ interface ResolvedBindings {
428
489
  * cross-binding joins mutually class-equal by transitivity.
429
490
  */
430
491
  function mintSlotOf(context: ChainContext, ref: AnyVar): ClassedField {
492
+ const facade = importFacadeOfOwner(ref.owner)
493
+ if (facade !== undefined) {
494
+ // An import var's mint slot is the imported head column's slot:
495
+ // the descriptor it projected and the carrier class that survived
496
+ // the projection (aggregate outputs are bare derived scalars).
497
+ const column = facade.columns.get(ref.column)
498
+ if (column === undefined || (facade.source.schema as AnySchema) !== context.theory) {
499
+ throw new AuthoringError({
500
+ message: `the variable ${ref.label} was minted from a query template schema ${context.theory.name} does not own`
501
+ })
502
+ }
503
+ return column.slot ?? { field: ref.field, class: undefined }
504
+ }
431
505
  if (context.theory.relations[ref.owner.name] !== ref.owner) {
432
- throw errors.new(
433
- `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`
434
- )
506
+ 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`
508
+ })
435
509
  }
436
510
  return { field: ref.field, class: context.classes[ref.owner.name]?.[ref.column] }
437
511
  }
@@ -443,17 +517,19 @@ function membershipSet(
443
517
  ): { readonly name: string; readonly members: readonly string[] } {
444
518
  const roster = rosterOf(field)
445
519
  if (roster === undefined) {
446
- throw errors.new(
447
- `${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`
448
- )
520
+ throw new AuthoringError({
521
+ message: `${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`
522
+ })
449
523
  }
450
524
  if (value.length === 0) {
451
- throw errors.new(`${context}: an empty membership array selects nothing — write the query you mean`)
525
+ throw new AuthoringError({
526
+ message: `${context}: an empty membership array selects nothing — write the query you mean`
527
+ })
452
528
  }
453
529
  if (value.length === 1) {
454
- throw errors.new(
455
- `${context}: a one-element membership array is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`
456
- )
530
+ throw new AuthoringError({
531
+ message: `${context}: a one-element membership array is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`
532
+ })
457
533
  }
458
534
  const seen = new Set<string>()
459
535
  const members = value.map(function memberName(member) {
@@ -461,9 +537,9 @@ function membershipSet(
461
537
  throw literalShapeError(context, `a ${roster.name} handle name (string)`, member)
462
538
  }
463
539
  if (seen.has(member)) {
464
- throw errors.new(
465
- `${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
466
- )
540
+ throw new AuthoringError({
541
+ message: `${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
542
+ })
467
543
  }
468
544
  seen.add(member)
469
545
  return member
@@ -492,7 +568,7 @@ function resolveBindings(
492
568
  return candidate.name === fieldName
493
569
  })
494
570
  if (declared === undefined) {
495
- throw errors.new(`${label} has no field ${fieldName}`)
571
+ throw new AuthoringError({ message: `${label} has no field ${fieldName}` })
496
572
  }
497
573
  const fieldClass = relationClasses?.[fieldName]
498
574
  let bound: BindingEntry["term"]
@@ -503,9 +579,9 @@ function resolveBindings(
503
579
  const mint = mintSlotOf(context, ref)
504
580
  const positionSlot: ClassedField = { field: declared.field, class: fieldClass }
505
581
  if (!joins(mint, positionSlot)) {
506
- throw errors.new(
507
- `${label}: the variable ${ref.label} joins domain-unequal fields — minted at ${renderFieldKind(mint)}, reused at ${renderFieldKind(positionSlot)} (a var joins only class-equal slots; bare pairs only with bare)`
508
- )
582
+ throw new AuthoringError({
583
+ message: `${label}: the variable ${ref.label} joins domain-unequal fields — minted at ${renderFieldKind(mint)}, reused at ${renderFieldKind(positionSlot)} (a var joins only class-equal slots; bare pairs only with bare)`
584
+ })
509
585
  }
510
586
  bound = Object.freeze({ kind: "var" as const, ref })
511
587
  vars.push(ref)
@@ -615,7 +691,9 @@ function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
615
691
  if (cond.op === "allen") {
616
692
  const maskValue = cond.mask
617
693
  if (typeof maskValue !== "number") {
618
- throw errors.new("allen: the mask position takes a 13-bit mask number built from the ALLEN constants")
694
+ throw new AuthoringError({
695
+ message: "allen: the mask position takes a 13-bit mask number built from the ALLEN constants"
696
+ })
619
697
  }
620
698
  return Object.freeze({
621
699
  kind: "cmp" as const,
@@ -632,14 +710,14 @@ function condDataOf(cond: AnyCond, uses: ParamUse[]): CondData {
632
710
  })
633
711
  return Object.freeze({ kind: "tree" as const, op: cond.op, children: Object.freeze(children) })
634
712
  }
635
- throw errors.new(
636
- "a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()"
637
- )
713
+ throw new AuthoringError({
714
+ message: "a negated atom is not a condition-tree node — pass not(...) to where() directly, never inside and()/or()"
715
+ })
638
716
  }
639
717
 
640
718
  function advanceWhere(context: ChainContext, state: RuleBuildState, cond: AnyCond): RuleBuildState {
641
719
  if (typeof cond !== "object" || cond === null || !("cond" in cond)) {
642
- throw errors.new("where() takes a comparison, an and()/or() tree, or a negated atom")
720
+ throw new AuthoringError({ message: "where() takes a comparison, an and()/or() tree, or a negated atom" })
643
721
  }
644
722
  if (cond.cond === "notInterior") {
645
723
  const bindings: Readonly<Record<string, unknown>> = Object.fromEntries(
@@ -684,9 +762,9 @@ function advanceInterior(
684
762
  continue
685
763
  }
686
764
  if (!isTerm(value) || value[term] !== "var") {
687
- throw errors.new(
688
- `interior ${target.name}: position ${key} takes a variable — bind literals and params through where()/match()`
689
- )
765
+ throw new AuthoringError({
766
+ message: `interior ${target.name}: position ${key} takes a variable — bind literals and params through where()/match()`
767
+ })
690
768
  }
691
769
  resolved.push(Object.freeze({ key, ref: value }))
692
770
  }
@@ -712,7 +790,7 @@ function asVarTerm(context: string, value: unknown): AnyVar {
712
790
  if (isTerm(value) && value[term] === "var") {
713
791
  return value
714
792
  }
715
- throw errors.new(`${context}: expected a variable`)
793
+ throw new AuthoringError({ message: `${context}: expected a variable` })
716
794
  }
717
795
 
718
796
  function aggDataOf(name: string, entry: { readonly agg: string; readonly over?: unknown }): AggData {
@@ -722,17 +800,18 @@ function aggDataOf(name: string, entry: { readonly agg: string; readonly over?:
722
800
  const over = entry.over
723
801
  switch (entry.agg) {
724
802
  case "sum":
803
+ case "mean":
725
804
  case "min":
726
805
  case "max": {
727
806
  if (isTerm(over) && over[term] === "var") {
728
807
  return Object.freeze({ op: "fold" as const, fold: entry.agg, over })
729
808
  }
730
- throw errors.new(`find ${name} (${entry.agg}): takes a variable`)
809
+ throw new AuthoringError({ message: `find ${name} (${entry.agg}): takes a variable` })
731
810
  }
732
811
  case "pack":
733
812
  return Object.freeze({ op: "pack" as const, over: asVarTerm(`find ${name} (pack)`, over) })
734
813
  default:
735
- throw errors.new(`find ${name}: unknown aggregate ${entry.agg}`)
814
+ throw new AuthoringError({ message: `find ${name}: unknown aggregate ${entry.agg}` })
736
815
  }
737
816
  }
738
817
 
@@ -746,7 +825,17 @@ function findColumnOf(name: string, entry: unknown): FindColumn {
746
825
  slot: undefined
747
826
  })
748
827
  }
749
- throw errors.new(`find ${name}: a ${entry[term]} is not projectable — find takes variables or aggregates`)
828
+ throw new AuthoringError({
829
+ message: `find ${name}: a ${entry[term]} is not projectable — find takes variables, aggregates or Compute expressions`
830
+ })
831
+ }
832
+ if (isComputeExpr(entry)) {
833
+ return Object.freeze({
834
+ name,
835
+ entry: Object.freeze({ kind: "compute" as const, expr: entry, result: entry.result }),
836
+ closed: undefined,
837
+ slot: undefined
838
+ })
750
839
  }
751
840
  if (isAggregateEntry(entry)) {
752
841
  return Object.freeze({
@@ -756,7 +845,9 @@ function findColumnOf(name: string, entry: unknown): FindColumn {
756
845
  slot: undefined
757
846
  })
758
847
  }
759
- throw errors.new(`find ${name}: not a find entry — find takes variables or aggregates`)
848
+ throw new AuthoringError({
849
+ message: `find ${name}: not a find entry — find takes variables, aggregates or Compute expressions`
850
+ })
760
851
  }
761
852
 
762
853
  /**
@@ -764,9 +855,9 @@ function findColumnOf(name: string, entry: unknown): FindColumn {
764
855
  * § orderability): a closed reference is equality-and-membership only.
765
856
  */
766
857
  function closedOrderError(context: string, position: string, vocabulary: string): Error {
767
- return errors.new(
768
- `${context}: ${position} is a ${vocabulary} reference — declaration order is an accident, not semantics: vocabularies do not order (equality, membership, and counting remain)`
769
- )
858
+ return new AuthoringError({
859
+ message: `${context}: ${position} is a ${vocabulary} reference — declaration order is an accident, not semantics: vocabularies do not order (equality, membership, and counting remain)`
860
+ })
770
861
  }
771
862
 
772
863
  function isOrderOp(op: CmpKind | "binding"): op is "lt" | "le" | "gt" | "ge" | "pointIn" {
@@ -775,15 +866,17 @@ function isOrderOp(op: CmpKind | "binding"): op is "lt" | "le" | "gt" | "ge" | "
775
866
 
776
867
  function assertBound(where: string, bound: ReadonlySet<AnyVar>, ref: AnyVar): void {
777
868
  if (!bound.has(ref)) {
778
- throw errors.new(`${where}: the variable ${ref.label} is not bound by a relation atom of the rule`)
869
+ throw new AuthoringError({
870
+ message: `${where}: the variable ${ref.label} is not bound by a relation atom of the rule`
871
+ })
779
872
  }
780
873
  }
781
874
 
782
875
  function assertInterval(where: string, ref: AnyVar): void {
783
876
  if (ref.field.kind !== "interval") {
784
- throw errors.new(
785
- `${where}: ${ref.label} is not interval-typed — the measure is defined over interval-typed variables only`
786
- )
877
+ throw new AuthoringError({
878
+ message: `${where}: ${ref.label} is not interval-typed — the measure is defined over interval-typed variables only`
879
+ })
787
880
  }
788
881
  }
789
882
 
@@ -795,17 +888,41 @@ function assertNotClosed(where: string, position: string, ref: AnyVar): void {
795
888
  }
796
889
 
797
890
  function assertNumeric(where: string, position: string, ref: AnyVar): void {
798
- if (ref.field.kind !== "u64" && ref.field.kind !== "i64") {
799
- throw errors.new(`${where}: ${position} ${ref.label} is ${ref.field.kind}, not numeric — a fold reads u64/i64 only`)
891
+ if (ref.field.kind !== "u64" && ref.field.kind !== "i64" && ref.field.kind !== "f64") {
892
+ throw new AuthoringError({
893
+ message: `${where}: ${position} ${ref.label} is ${ref.field.kind}, not numeric — a fold reads u64/i64/f64 only`
894
+ })
800
895
  }
801
896
  }
802
897
 
898
+ /**
899
+ * The head column's slot: a projected variable keeps its mint slot; an
900
+ * aggregate output is a NEW derived scalar — its descriptor follows the
901
+ * aggregate's typing (count is u64, mean is f64, sum/min/max keep the
902
+ * input's scalar kind, pack keeps the interval kind) and its class is
903
+ * BARE: a derived value is not the carrier the class law tracked.
904
+ */
803
905
  function findColumnSlotOf(context: ChainContext, column: FindColumn): ClassedField | undefined {
804
906
  const entry = column.entry
805
907
  if (entry.kind === "var") {
806
908
  return mintSlotOf(context, entry.over)
807
909
  }
808
- return undefined
910
+ if (entry.kind === "compute") {
911
+ return { field: computeFieldOf(entry.result), class: undefined }
912
+ }
913
+ const agg = entry.agg
914
+ switch (agg.op) {
915
+ case "count":
916
+ return { field: u64Field, class: undefined }
917
+ case "fold": {
918
+ if (agg.fold === "mean") {
919
+ return { field: f64Field, class: undefined }
920
+ }
921
+ return { field: agg.over.field, class: undefined }
922
+ }
923
+ case "pack":
924
+ return { field: agg.over.field, class: undefined }
925
+ }
809
926
  }
810
927
 
811
928
  function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, column: FindColumn): void {
@@ -815,6 +932,15 @@ function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, colum
815
932
  assertBound(where, bound, entry.over)
816
933
  return
817
934
  }
935
+ if (entry.kind === "compute") {
936
+ // Kind agreement, closed-reference and depth walls hold at
937
+ // construction (#query/compute.ts); boundness is a RULE property,
938
+ // judged here like every other term position.
939
+ for (const ref of computeVarsOf(entry.expr)) {
940
+ assertBound(where, bound, ref)
941
+ }
942
+ return
943
+ }
818
944
  const agg = entry.agg
819
945
  switch (agg.op) {
820
946
  case "count":
@@ -823,6 +949,9 @@ function validateColumn(context: ChainContext, bound: ReadonlySet<AnyVar>, colum
823
949
  assertBound(where, bound, agg.over)
824
950
  assertNotClosed(where, `the ${agg.fold} input`, agg.over)
825
951
  assertNumeric(where, `the ${agg.fold} input`, agg.over)
952
+ if (agg.fold === "mean" && agg.over.field.kind !== "f64") {
953
+ throw new AuthoringError({ message: `${where}: mean requires an f64 input; cast integers explicitly` })
954
+ }
826
955
  return
827
956
  }
828
957
  case "pack":
@@ -850,9 +979,9 @@ function validateCond(context: ChainContext, bound: ReadonlySet<AnyVar>, cond: C
850
979
  const lhs = mintSlotOf(context, cond.lhs.ref)
851
980
  const rhs = mintSlotOf(context, cond.rhs.ref)
852
981
  if (!fieldJoins(lhs, rhs)) {
853
- throw errors.new(
854
- `${label}: ${cond.op.kind}(${cond.lhs.ref.label}, ${cond.rhs.ref.label}) unifies domain-unequal fields — ${cond.lhs.ref.label} bound at ${renderFieldKind(lhs)}, ${cond.rhs.ref.label} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
855
- )
982
+ throw new AuthoringError({
983
+ message: `${label}: ${cond.op.kind}(${cond.lhs.ref.label}, ${cond.rhs.ref.label}) unifies domain-unequal fields — ${cond.lhs.ref.label} bound at ${renderFieldKind(lhs)}, ${cond.rhs.ref.label} at ${renderFieldKind(rhs)} (a var joins only class-equal slots; bare pairs only with bare)`
984
+ })
856
985
  }
857
986
  }
858
987
  return
@@ -882,23 +1011,23 @@ function validateInterior(
882
1011
  })
883
1012
  for (const key of keys) {
884
1013
  if (!headNames.includes(key)) {
885
- throw errors.new(
886
- `${label}: interior ${item.target.name} binds ${key}, not a head column of ${item.target.name} (head columns: ${headNames.join(", ")})`
887
- )
1014
+ throw new AuthoringError({
1015
+ message: `${label}: interior ${item.target.name} binds ${key}, not a head column of ${item.target.name} (head columns: ${headNames.join(", ")})`
1016
+ })
888
1017
  }
889
1018
  }
890
1019
  for (const name of headNames) {
891
1020
  if (!keys.includes(name)) {
892
- throw errors.new(
893
- `${label}: interior ${item.target.name} omits the head column ${name} — an interior join binds every head column of ${item.target.name}`
894
- )
1021
+ throw new AuthoringError({
1022
+ message: `${label}: interior ${item.target.name} omits the head column ${name} — an interior join binds every head column of ${item.target.name}`
1023
+ })
895
1024
  }
896
1025
  }
897
1026
  for (const binding of item.bindings) {
898
1027
  if (item.kind === "negatedInterior" && !bound.has(binding.ref)) {
899
- throw errors.new(
900
- `${label}: negated interior ${item.target.name} names the variable ${binding.ref.label}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
901
- )
1028
+ throw new AuthoringError({
1029
+ message: `${label}: negated interior ${item.target.name} names the variable ${binding.ref.label}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
1030
+ })
902
1031
  }
903
1032
  const headColumn = headColumns.find(function byName(column) {
904
1033
  return column.name === binding.key
@@ -908,9 +1037,9 @@ function validateInterior(
908
1037
  }
909
1038
  const mint = mintSlotOf(context, binding.ref)
910
1039
  if (!fieldJoins(headColumn.slot, mint)) {
911
- throw errors.new(
912
- `${label}: interior ${item.target.name} joins the variable ${binding.ref.label} (${renderFieldKind(mint)}) at head column ${binding.key} (${renderFieldKind(headColumn.slot)}) — a var joins only class-equal slots; bare pairs only with bare`
913
- )
1040
+ throw new AuthoringError({
1041
+ message: `${label}: interior ${item.target.name} joins the variable ${binding.ref.label} (${renderFieldKind(mint)}) at head column ${binding.key} (${renderFieldKind(headColumn.slot)}) — a var joins only class-equal slots; bare pairs only with bare`
1042
+ })
914
1043
  }
915
1044
  }
916
1045
  }
@@ -918,7 +1047,7 @@ function validateInterior(
918
1047
  function completeRule(context: ChainContext, state: RuleBuildState, rawColumns: readonly FindColumn[]): RuleData {
919
1048
  const label = contextLabel(context)
920
1049
  if (rawColumns.length === 0) {
921
- throw errors.new(`${label}: a find needs at least one entry`)
1050
+ throw new AuthoringError({ message: `${label}: a find needs at least one entry` })
922
1051
  }
923
1052
  const columns = rawColumns.map(function enrichColumn(column): FindColumn {
924
1053
  assertDeclarationOrderKey(`${label} find column`, column.name)
@@ -930,9 +1059,9 @@ function completeRule(context: ChainContext, state: RuleBuildState, rawColumns:
930
1059
  if (item.kind === "negated") {
931
1060
  for (const binding of item.atom.bindings) {
932
1061
  if (binding.term.kind === "var" && !state.bound.has(binding.term.ref)) {
933
- throw errors.new(
934
- `${label}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.ref.label} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
935
- )
1062
+ throw new AuthoringError({
1063
+ message: `${label}: negated ${item.atom.relation.name} atom binds the variable ${binding.term.ref.label} at position ${binding.field}, but no positive atom of the rule binds it — a negated atom binds nothing, only rejects (the safety rule)`
1064
+ })
936
1065
  }
937
1066
  }
938
1067
  }
@@ -951,14 +1080,14 @@ function makeRuleValue<Row, P extends ParamsRecord>(rule: RuleData): RuleValue<R
951
1080
  }
952
1081
 
953
1082
  interface RawChain {
954
- match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1083
+ match(relation: MatchOwner | AnyQuery, bindings: Readonly<Record<string, unknown>>): RawChain
955
1084
  where(cond: AnyCond): RawChain
956
1085
  interior(name: string, bindings: Readonly<Record<string, unknown>>): RawChain
957
1086
  find(entries: Readonly<Record<string, unknown>>): RuleValue<never, never>
958
1087
  }
959
1088
 
960
1089
  interface RawScope extends TermOps {
961
- match(relation: MatchOwner, bindings: Readonly<Record<string, unknown>>): RawChain
1090
+ match(relation: MatchOwner | AnyQuery, bindings: Readonly<Record<string, unknown>>): RawChain
962
1091
  interior(name: string, bindings: Readonly<Record<string, unknown>>): RawChain
963
1092
  }
964
1093
 
@@ -997,26 +1126,30 @@ function lookupDerived(context: ChainContext, name: string): DerivedTable {
997
1126
  })
998
1127
  if (interior !== undefined) {
999
1128
  if (context.kind === "interior" && name === context.self) {
1000
- throw errors.new(
1001
- `interior ${name}: an interior does not read itself — declaration order is topological (a self-read is InteriorNotPrior)`
1002
- )
1129
+ throw new AuthoringError({
1130
+ message: `interior ${name}: an interior does not read itself — declaration order is topological (a self-read is InteriorNotPrior)`
1131
+ })
1003
1132
  }
1004
1133
  return interior
1005
1134
  }
1006
1135
  const rec = "rec" in context ? context.rec : undefined
1007
1136
  if (rec !== undefined && rec.name === name) {
1008
1137
  if (context.kind === "interior") {
1009
- throw errors.new(`interior ${context.self}: interiors cannot read the rec — this cut's interiors are a prefix`)
1138
+ throw new AuthoringError({
1139
+ message: `interior ${context.self}: interiors cannot read the rec — this cut's interiors are a prefix`
1140
+ })
1010
1141
  }
1011
1142
  if (context.kind === "rec-base") {
1012
- throw errors.new(`rec ${rec.name}: a base arm does not read the rec — self-atoms belong on rec arms`)
1143
+ throw new AuthoringError({
1144
+ message: `rec ${rec.name}: a base arm does not read the rec — self-atoms belong on rec arms`
1145
+ })
1013
1146
  }
1014
1147
  if (!isRecHead(rec)) {
1015
- throw errors.new(`rec ${rec.name}: rec arms resolve the rec head after base arms seal it`)
1148
+ throw new AuthoringError({ message: `rec ${rec.name}: rec arms resolve the rec head after base arms seal it` })
1016
1149
  }
1017
1150
  return rec
1018
1151
  }
1019
- throw errors.new(`${contextLabel(context)}: no derived table named ${name} is in scope`)
1152
+ throw new AuthoringError({ message: `${contextLabel(context)}: no derived table named ${name} is in scope` })
1020
1153
  }
1021
1154
 
1022
1155
  function interiorAdvance(
@@ -1028,6 +1161,159 @@ function interiorAdvance(
1028
1161
  return advanceInterior(state, lookupDerived(context, name), bindings, "interior")
1029
1162
  }
1030
1163
 
1164
+ /**
1165
+ * Imported query templates as relation-expression sources (chapter 34's
1166
+ * nonrecursive composition). The imported CQ's whole body — its inner
1167
+ * interiors plus its main rules as one derived table — is SPLICED into the
1168
+ * consuming query's interior roster at assembly ({@link makeRawQuery});
1169
+ * naming does not materialize anything, and the imported template keeps its
1170
+ * owned immutable IR. One entry is minted per imported query VALUE, so two
1171
+ * references to the same template read one derived table.
1172
+ */
1173
+ interface ImportEntry {
1174
+ readonly table: InteriorData
1175
+ readonly inner: readonly InteriorData[]
1176
+ }
1177
+
1178
+ const importEntries = new WeakMap<object, ImportEntry>()
1179
+ const importTables = new WeakSet<InteriorData>()
1180
+
1181
+ function importEntryOf(context: ChainContext, imported: AnyQuery): ImportEntry {
1182
+ const cached = importEntries.get(imported)
1183
+ if (cached !== undefined) {
1184
+ if (imported.schema !== context.theory) {
1185
+ throw new AuthoringError({
1186
+ message: `${contextLabel(context)}: the imported query belongs to schema ${imported.schema.name}, not ${context.theory.name} — templates compose within one schema`
1187
+ })
1188
+ }
1189
+ return cached
1190
+ }
1191
+ if (imported.schema !== context.theory) {
1192
+ throw new AuthoringError({
1193
+ message: `${contextLabel(context)}: the imported query belongs to schema ${imported.schema.name}, not ${context.theory.name} — templates compose within one schema`
1194
+ })
1195
+ }
1196
+ if (imported.data.kind === "reach") {
1197
+ throw new AuthoringError({
1198
+ 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
+ })
1200
+ }
1201
+ const userParams = imported.data.params.filter(function userSupplied(entry) {
1202
+ return entry.membership === undefined
1203
+ })
1204
+ if (userParams.length > 0) {
1205
+ throw new AuthoringError({
1206
+ message: `${contextLabel(context)}: the imported query takes parameters (${userParams
1207
+ .map((entry) => entry.name)
1208
+ .join(", ")}) — bind an import's meaning at declaration; parameterized imports are not supported`
1209
+ })
1210
+ }
1211
+ const facade = importFacadeOf(imported)
1212
+ const table: InteriorData = Object.freeze({
1213
+ name: facade.owner.name,
1214
+ finds: imported.data.finds,
1215
+ rules: imported.data.rules
1216
+ })
1217
+ const entry: ImportEntry = Object.freeze({ table, inner: imported.data.interiors })
1218
+ importEntries.set(imported, entry)
1219
+ importTables.add(table)
1220
+ importInner.set(table, entry.inner)
1221
+ return entry
1222
+ }
1223
+
1224
+ const importInner = new WeakMap<InteriorData, readonly InteriorData[]>()
1225
+
1226
+ /**
1227
+ * Every import table the assembled query references, in first-reference
1228
+ * order (declared interiors first, then rec arms, then main rules — the
1229
+ * lowering walk), deduplicated by table identity.
1230
+ */
1231
+ function referencedImports(
1232
+ interiors: readonly InteriorData[],
1233
+ rec: RecData | undefined,
1234
+ rules: readonly RuleData[]
1235
+ ): readonly InteriorData[] {
1236
+ const seen = new Set<InteriorData>()
1237
+ const out: InteriorData[] = []
1238
+ function walk(ruleList: readonly RuleData[]): void {
1239
+ for (const rule of ruleList) {
1240
+ for (const item of rule.items) {
1241
+ if (item.kind !== "interior" && item.kind !== "negatedInterior") {
1242
+ continue
1243
+ }
1244
+ const target = item.target as InteriorData
1245
+ if (importTables.has(target) && !seen.has(target)) {
1246
+ seen.add(target)
1247
+ out.push(target)
1248
+ }
1249
+ }
1250
+ }
1251
+ }
1252
+ for (const interior of interiors) {
1253
+ walk(interior.rules)
1254
+ }
1255
+ if (rec !== undefined) {
1256
+ walk(rec.base)
1257
+ walk(rec.rec)
1258
+ }
1259
+ walk(rules)
1260
+ return out
1261
+ }
1262
+
1263
+ /**
1264
+ * Splices referenced imports ahead of the declared interiors: each import's
1265
+ * inner interiors first (its rules read them), then the import's own table.
1266
+ * Interior prior-ness is preserved — declared interiors never read imports
1267
+ * they could not have named, and main/rec rules may read anything earlier.
1268
+ * Duplicate names across the final roster refuse.
1269
+ */
1270
+ function spliceImports(
1271
+ interiors: readonly InteriorData[],
1272
+ rec: RecData | undefined,
1273
+ rules: readonly RuleData[]
1274
+ ): readonly InteriorData[] {
1275
+ const imports = referencedImports(interiors, rec, rules)
1276
+ if (imports.length === 0) {
1277
+ return interiors
1278
+ }
1279
+ const spliced: InteriorData[] = []
1280
+ const names = new Set<string>()
1281
+ const added = new Set<InteriorData>()
1282
+ function add(table: InteriorData): void {
1283
+ if (added.has(table)) {
1284
+ return
1285
+ }
1286
+ if (names.has(table.name)) {
1287
+ throw new AuthoringError({
1288
+ message: `query: the derived-table name ${table.name} appears twice after import splicing — rename the colliding interior (names are unique)`
1289
+ })
1290
+ }
1291
+ added.add(table)
1292
+ names.add(table.name)
1293
+ spliced.push(table)
1294
+ }
1295
+ for (const table of imports) {
1296
+ for (const inner of importInner.get(table) ?? []) {
1297
+ add(inner)
1298
+ }
1299
+ add(table)
1300
+ }
1301
+ for (const declared of interiors) {
1302
+ add(declared)
1303
+ }
1304
+ return Object.freeze(spliced)
1305
+ }
1306
+
1307
+ function importAdvance(
1308
+ context: ChainContext,
1309
+ state: RuleBuildState,
1310
+ imported: AnyQuery,
1311
+ bindings: Readonly<Record<string, unknown>>
1312
+ ): RuleBuildState {
1313
+ const entry = importEntryOf(context, imported)
1314
+ return advanceInterior(state, entry.table, bindings, "interior")
1315
+ }
1316
+
1031
1317
  function notInteriorAdvance(
1032
1318
  context: ChainContext,
1033
1319
  state: RuleBuildState,
@@ -1035,25 +1321,28 @@ function notInteriorAdvance(
1035
1321
  bindings: Readonly<Record<string, unknown>>
1036
1322
  ): RuleBuildState {
1037
1323
  if (context.kind === "rec-base" || context.kind === "rec-arm") {
1038
- throw errors.new(
1039
- `rec ${context.self.name}: a rec rule negates no table — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished table's fold belongs in the main rules`
1040
- )
1324
+ throw new AuthoringError({
1325
+ message: `rec ${context.self.name}: a rec rule negates no table — self-negation is negation through the cycle (a finished set is what keeps the operator monotone), and a finished table's fold belongs in the main rules`
1326
+ })
1041
1327
  }
1042
1328
  return advanceInterior(state, lookupDerived(context, name), bindings, "negatedInterior")
1043
1329
  }
1044
1330
 
1045
1331
  function findColumns(context: ChainContext, entries: Readonly<Record<string, unknown>>): FindColumn[] {
1046
1332
  const columns: FindColumn[] = []
1047
- const derivedHead = context.kind !== "query"
1333
+ // Nonrecursive derived stages (interiors) may emit aggregate/computed
1334
+ // outputs (C05: "do not leave Interior.rules projection only"); ONLY the
1335
+ // recursive feedback cycle stays projection-only — no aggregate,
1336
+ // arithmetic-created value or negation flows through it.
1337
+ const recName = context.kind === "rec-base" || context.kind === "rec-arm" ? context.self.name : undefined
1048
1338
  for (const [name, entry] of Object.entries(entries)) {
1049
1339
  if (entry === undefined) {
1050
1340
  continue
1051
1341
  }
1052
- if (derivedHead && !(isTerm(entry) && entry[term] === "var")) {
1053
- const who = context.kind === "interior" ? `interior ${context.self}` : `rec ${context.self.name}`
1054
- throw errors.new(
1055
- `${who}: a rec head projects bound variables only — aggregates and the measure read finished sets (unwritable here)`
1056
- )
1342
+ if (recName !== undefined && !(isTerm(entry) && entry[term] === "var")) {
1343
+ throw new AuthoringError({
1344
+ 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)`
1345
+ })
1057
1346
  }
1058
1347
  columns.push(findColumnOf(name, entry))
1059
1348
  }
@@ -1063,7 +1352,10 @@ function findColumns(context: ChainContext, entries: Readonly<Record<string, unk
1063
1352
  function makeRawChain(context: ChainContext, state: RuleBuildState): RawChain {
1064
1353
  const chain: RawChain = {
1065
1354
  match(relation, bindings) {
1066
- return makeRawChain(context, advanceMatch(context, state, relation, bindings))
1355
+ if (isImportedSource(relation)) {
1356
+ return makeRawChain(context, importAdvance(context, state, relation as unknown as AnyQuery, bindings))
1357
+ }
1358
+ return makeRawChain(context, advanceMatch(context, state, relation as MatchOwner, bindings))
1067
1359
  },
1068
1360
  where(cond) {
1069
1361
  return makeRawChain(context, advanceWhere(context, state, cond))
@@ -1083,7 +1375,10 @@ function makeRawScope(context: ChainContext): RawScope {
1083
1375
  const scope: RawScope = {
1084
1376
  ...termOps,
1085
1377
  match(relation, bindings) {
1086
- return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation, bindings))
1378
+ if (isImportedSource(relation)) {
1379
+ return makeRawChain(context, importAdvance(context, EMPTY_RULE, relation as unknown as AnyQuery, bindings))
1380
+ }
1381
+ return makeRawChain(context, advanceMatch(context, EMPTY_RULE, relation as MatchOwner, bindings))
1087
1382
  },
1088
1383
  interior(name, bindings) {
1089
1384
  return makeRawChain(context, interiorAdvance(context, EMPTY_RULE, name, bindings))
@@ -1112,7 +1407,7 @@ function makeQueryRuleScope<Rels extends SchemaRelations, Classes extends Schema
1112
1407
  ): QueryRuleScope<Rels, Classes> {
1113
1408
  const raw = makeRawScope({ kind: "query", classes: theory.classes, theory, ...env })
1114
1409
  if (!isTypedScope<QueryRuleScope<Rels, Classes>>(raw)) {
1115
- throw errors.new("query rule builder construction incomplete")
1410
+ throw new SdkInvariantError({ message: "query rule builder construction incomplete" })
1116
1411
  }
1117
1412
  return raw
1118
1413
  }
@@ -1124,7 +1419,7 @@ function makeInteriorRuleScope<Rels extends SchemaRelations, Classes extends Sch
1124
1419
  ): InteriorRuleScope<Rels, Classes> {
1125
1420
  const raw = makeRawScope({ kind: "interior", self, classes: theory.classes, theory, ...env })
1126
1421
  if (!isTypedScope<InteriorRuleScope<Rels, Classes>>(raw)) {
1127
- throw errors.new("interior rule builder construction incomplete")
1422
+ throw new SdkInvariantError({ message: "interior rule builder construction incomplete" })
1128
1423
  }
1129
1424
  return raw
1130
1425
  }
@@ -1164,7 +1459,7 @@ function makeRecRuleScope<Rels extends SchemaRelations, Classes extends SchemaCl
1164
1459
  ...env
1165
1460
  })
1166
1461
  if (!isTypedScope<RecRuleScope<Rels, Classes>>(raw)) {
1167
- throw errors.new("rec rule builder construction incomplete")
1462
+ throw new SdkInvariantError({ message: "rec rule builder construction incomplete" })
1168
1463
  }
1169
1464
  return raw
1170
1465
  }
@@ -1178,6 +1473,9 @@ function headSignature(column: FindColumn): string {
1178
1473
  if (entry.kind === "var") {
1179
1474
  return `${column.name}:var`
1180
1475
  }
1476
+ if (entry.kind === "compute") {
1477
+ return `${column.name}:compute`
1478
+ }
1181
1479
  const agg = entry.agg
1182
1480
  if (agg.op === "fold") {
1183
1481
  return `${column.name}:${agg.fold}`
@@ -1230,22 +1528,22 @@ function paramRegistryOf(
1230
1528
  continue
1231
1529
  }
1232
1530
  if ((existing.members === undefined) !== (use.members === undefined)) {
1233
- throw errors.new(
1234
- `query param ${use.name} collides with a membership array's registry entry — name the param differently`
1235
- )
1531
+ throw new AuthoringError({
1532
+ message: `query param ${use.name} collides with a membership array's registry entry — name the param differently`
1533
+ })
1236
1534
  }
1237
1535
  if (existing.shape !== use.shape) {
1238
- throw errors.new(
1239
- `query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`
1240
- )
1536
+ throw new AuthoringError({
1537
+ message: `query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`
1538
+ })
1241
1539
  }
1242
1540
  if (existing.anchor !== undefined && use.anchor !== undefined) {
1243
1541
  const registered = rosterOf(existing.anchor)
1244
1542
  const anchored = rosterOf(use.anchor)
1245
1543
  if (registered !== anchored) {
1246
- throw errors.new(
1247
- `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`
1248
- )
1544
+ throw new AuthoringError({
1545
+ 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
+ })
1249
1547
  }
1250
1548
  }
1251
1549
  if (existing.anchor === undefined && use.anchor !== undefined) {
@@ -1274,14 +1572,14 @@ function paramRegistryOf(
1274
1572
  order.map(function entryOf(name): ParamEntry {
1275
1573
  const entry = byName.get(name)
1276
1574
  if (entry === undefined) {
1277
- throw errors.new(`query param ${name} lost its registry entry`)
1575
+ throw new AuthoringError({ message: `query param ${name} lost its registry entry` })
1278
1576
  }
1279
1577
 
1280
1578
  let membership: QueryParam | undefined
1281
1579
  if (entry.members !== undefined) {
1282
1580
  const anchor = entry.anchor
1283
1581
  if (anchor === undefined) {
1284
- throw errors.new(`query param ${name} lost its membership anchor`)
1582
+ throw new AuthoringError({ message: `query param ${name} lost its membership anchor` })
1285
1583
  }
1286
1584
  membership = Object.freeze({
1287
1585
  kind: "set" as const,
@@ -1301,6 +1599,7 @@ interface RawQuery {
1301
1599
  readonly schema: AnySchema
1302
1600
  readonly data: QueryData
1303
1601
  rule(build: (r: RawScope) => RuleValue<never, never>): RawQuery
1602
+ named(label: string): RawQuery
1304
1603
  interior(name: string, ...builds: never[]): never
1305
1604
  reach(name: string, arms: never): never
1306
1605
  }
@@ -1316,30 +1615,30 @@ interface RawQuery {
1316
1615
  function alignedHeadOf(label: string, rules: readonly RuleData[]): readonly FindColumn[] {
1317
1616
  const first = rules[0]
1318
1617
  if (first === undefined) {
1319
- throw errors.new(`${label} needs at least one rule`)
1618
+ throw new AuthoringError({ message: `${label} needs at least one rule` })
1320
1619
  }
1321
1620
  const signature = first.finds.map(headSignature).join(", ")
1322
1621
  rules.forEach(function verifyHead(rule, index) {
1323
1622
  const candidate = rule.finds.map(headSignature).join(", ")
1324
1623
  if (candidate !== signature) {
1325
- throw errors.new(
1326
- `every rule of ${label} derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
1327
- )
1624
+ throw new AuthoringError({
1625
+ message: `every rule of ${label} derives the same head — rule 0 finds (${signature}), rule ${index} finds (${candidate})`
1626
+ })
1328
1627
  }
1329
1628
  rule.finds.forEach(function verifyClosedSlice(column, position) {
1330
1629
  const lead = first.finds[position]
1331
1630
  if (lead !== undefined && column.closed !== lead.closed) {
1332
- throw errors.new(
1333
- `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)`
1334
- )
1631
+ throw new AuthoringError({
1632
+ 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
+ })
1335
1634
  }
1336
1635
  if (lead === undefined) {
1337
1636
  return
1338
1637
  }
1339
1638
  if (lead.slot !== undefined && column.slot !== undefined && !headFieldJoins(lead.slot, column.slot)) {
1340
- throw errors.new(
1341
- `every rule of ${label} derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in rule ${index} (a head column joins class-equal slots; same-wire u64 admits a bare side, and the merged head is bare)`
1342
- )
1639
+ throw new AuthoringError({
1640
+ message: `every rule of ${label} derives the same head — the head column ${lead.name} is bound at ${renderFieldKind(lead.slot)} in rule 0 but at ${renderFieldKind(column.slot)} in rule ${index} (a head column joins class-equal slots; same-wire u64 admits a bare side, and the merged head is bare)`
1641
+ })
1343
1642
  }
1344
1643
  })
1345
1644
  })
@@ -1366,9 +1665,9 @@ function alignedHeadOf(label: string, rules: readonly RuleData[]): readonly Find
1366
1665
  }
1367
1666
 
1368
1667
  function afterMainError(what: string): Error {
1369
- return errors.new(
1370
- `query: ${what} after a main rule is unwritable — declaration order is interiors, then rec, then main`
1371
- )
1668
+ return new AuthoringError({
1669
+ message: `query: ${what} after a main rule is unwritable — declaration order is interiors, then rec, then main`
1670
+ })
1372
1671
  }
1373
1672
 
1374
1673
  function makeRawQuery(
@@ -1380,12 +1679,13 @@ function makeRawQuery(
1380
1679
  const mergedFinds = alignedHeadOf("a query", rules)
1381
1680
  const first = rules[0]
1382
1681
  if (first === undefined) {
1383
- throw errors.new("a query needs at least one rule")
1682
+ throw new AuthoringError({ message: "a query needs at least one rule" })
1384
1683
  }
1385
1684
  const env: DerivedEnv = rec === undefined ? { interiors } : { interiors, rec }
1386
- const frozenInteriors = Object.freeze([...interiors])
1685
+ const allInteriors = spliceImports(interiors, rec, rules)
1686
+ const frozenInteriors = Object.freeze([...allInteriors])
1387
1687
  const frozenRules = Object.freeze([...rules])
1388
- const params = paramRegistryOf(interiors, rec, rules)
1688
+ const params = paramRegistryOf(allInteriors, rec, rules)
1389
1689
  const data: QueryData =
1390
1690
  rec === undefined
1391
1691
  ? Object.freeze({
@@ -1410,6 +1710,12 @@ function makeRawQuery(
1410
1710
  const built = build(makeRawScope({ kind: "query", classes: theory.classes, theory, ...env }))
1411
1711
  return makeRawQuery(theory, interiors, rec, [...rules, built.rule])
1412
1712
  },
1713
+ named(label) {
1714
+ // A diagnostic name for composition/tracing — not a schema
1715
+ // relation, a persistent view or a separate CTE type.
1716
+ labelImport(value, label)
1717
+ return value
1718
+ },
1413
1719
  interior() {
1414
1720
  throw afterMainError("interior")
1415
1721
  },
@@ -1437,7 +1743,7 @@ function collectInterior<Rels extends SchemaRelations, Classes extends SchemaCla
1437
1743
  builds: readonly InteriorBuild<Rels, Classes>[]
1438
1744
  ): InteriorData {
1439
1745
  if (builds.length === 0) {
1440
- throw errors.new(`query: interior ${name} needs at least one rule`)
1746
+ throw new AuthoringError({ message: `query: interior ${name} needs at least one rule` })
1441
1747
  }
1442
1748
  const rules = builds.map(function buildRule(buildOne) {
1443
1749
  return buildOne(makeInteriorRuleScope<Rels, Classes>(theory, env, name)).rule
@@ -1454,10 +1760,10 @@ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>
1454
1760
  recBuilds: readonly RecBuild<Rels, Classes>[]
1455
1761
  ): RecData {
1456
1762
  if (baseBuilds.length === 0) {
1457
- throw errors.new(`query: rec ${name} has no base arms`)
1763
+ throw new AuthoringError({ message: `query: rec ${name} has no base arms` })
1458
1764
  }
1459
1765
  if (recBuilds.length === 0) {
1460
- throw errors.new(`query: rec ${name} has no rec arms`)
1766
+ throw new AuthoringError({ message: `query: rec ${name} has no rec arms` })
1461
1767
  }
1462
1768
  const handle: RecHandle = Object.freeze({ name })
1463
1769
  const baseEnv: DerivedEnv = { interiors, rec: handle }
@@ -1467,11 +1773,11 @@ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>
1467
1773
  const baseFinds = alignedHeadOf(`rec ${name}`, base)
1468
1774
  const first = base[0]
1469
1775
  if (first === undefined) {
1470
- throw errors.new(`query: rec ${name} has no base arms`)
1776
+ throw new AuthoringError({ message: `query: rec ${name} has no base arms` })
1471
1777
  }
1472
1778
  const firstFind = baseFinds[0]
1473
1779
  if (firstFind === undefined) {
1474
- throw errors.new(`query: rec ${name} has no head`)
1780
+ throw new AuthoringError({ message: `query: rec ${name} has no head` })
1475
1781
  }
1476
1782
  const finds: RecHead["finds"] = [firstFind, ...baseFinds.slice(1)]
1477
1783
  const head: RecHead = Object.freeze({ name, finds })
@@ -1482,12 +1788,12 @@ function collectRec<Rels extends SchemaRelations, Classes extends SchemaClasses>
1482
1788
  const mergedAll = alignedHeadOf(`rec ${name}`, [...base, ...rec])
1483
1789
  const mergedFirst = mergedAll[0]
1484
1790
  if (mergedFirst === undefined) {
1485
- throw errors.new(`query: rec ${name} has no head`)
1791
+ throw new AuthoringError({ message: `query: rec ${name} has no head` })
1486
1792
  }
1487
1793
  const sealedFinds: RecHead["finds"] = [mergedFirst, ...mergedAll.slice(1)]
1488
1794
  const firstRec = rec[0]
1489
1795
  if (firstRec === undefined) {
1490
- throw errors.new(`query: rec ${name} has no rec arms`)
1796
+ throw new AuthoringError({ message: `query: rec ${name} has no rec arms` })
1491
1797
  }
1492
1798
  const sealedBase: RecData["base"] = [first, ...base.slice(1)]
1493
1799
  const sealedRec: RecData["rec"] = [firstRec, ...rec.slice(1)]
@@ -1515,10 +1821,10 @@ function makeQueryStart<Rels extends SchemaRelations, Classes extends SchemaClas
1515
1821
  return interior.name === name
1516
1822
  })
1517
1823
  ) {
1518
- throw errors.new(`query: interior ${name} is already declared — names are unique`)
1824
+ throw new AuthoringError({ message: `query: interior ${name} is already declared — names are unique` })
1519
1825
  }
1520
1826
  if (name.length === 0) {
1521
- throw errors.new("query: an interior needs a name")
1827
+ throw new AuthoringError({ message: "query: an interior needs a name" })
1522
1828
  }
1523
1829
  const data = collectInterior(theory, env, name, builds)
1524
1830
  return makeQueryStart<Rels, Classes, Flatten<P & BuildsParams<Builds>>>(theory, [...interiors, data])
@@ -1532,10 +1838,10 @@ function makeQueryStart<Rels extends SchemaRelations, Classes extends SchemaClas
1532
1838
  return interior.name === name
1533
1839
  })
1534
1840
  ) {
1535
- throw errors.new(`query: interior and rec share the name ${name}`)
1841
+ throw new AuthoringError({ message: `query: interior and rec share the name ${name}` })
1536
1842
  }
1537
1843
  if (name.length === 0) {
1538
- throw errors.new("query: reach needs a name")
1844
+ throw new AuthoringError({ message: "query: reach needs a name" })
1539
1845
  }
1540
1846
  const data = collectRec(theory, interiors, name, arms.base, arms.rec)
1541
1847
  return makeQueryReachStart<Rels, Classes, Flatten<P & BuildsParams<Base> & BuildsParams<Step>>>(
@@ -1589,14 +1895,30 @@ function taggedHandleId(
1589
1895
  }
1590
1896
  const id = closed.handles.indexOf(value)
1591
1897
  if (id < 0) {
1592
- throw errors.new(
1593
- `${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
1594
- )
1898
+ throw new AuthoringError({
1899
+ message: `${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
1900
+ })
1595
1901
  }
1596
1902
  return { kind: "u64", value: BigInt(id) }
1597
1903
  }
1598
1904
 
1599
- function taggedAtElementDomain(context: string, element: "u64" | "i64", value: unknown): TaggedValue {
1905
+ 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
+ }
1600
1922
  if (typeof value === "bigint") {
1601
1923
  if (element === "u64") {
1602
1924
  return { kind: "u64", value }
@@ -1636,6 +1958,18 @@ function taggedLiteral(context: string, field: AnyField, value: unknown): Tagged
1636
1958
  }
1637
1959
  return { kind: "i64", value }
1638
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
+ }
1639
1973
  case "str": {
1640
1974
  if (typeof value !== "string") {
1641
1975
  throw literalShapeError(context, "string", value)
@@ -1677,6 +2011,9 @@ function taggedCmpLiteral(context: string, sibling: AnyField, value: unknown, op
1677
2011
  ) {
1678
2012
  return taggedAtElementDomain(context, sibling.kind, value)
1679
2013
  }
2014
+ if (op === "pointIn" && rosterOf(sibling) === undefined && sibling.kind === "f64" && isFloatIntervalValue(value)) {
2015
+ return taggedAtElementDomain(context, "f64", value)
2016
+ }
1680
2017
  return taggedLiteral(context, sibling, value)
1681
2018
  }
1682
2019
 
@@ -1710,7 +2047,7 @@ function freshVarIds(): VarIds {
1710
2047
  function paramIdOf(ctx: LowerContext, name: string): number {
1711
2048
  const id = ctx.paramIds.get(name)
1712
2049
  if (id === undefined) {
1713
- throw errors.new(`query lowering: param ${name} is not in the query's registry`)
2050
+ throw new AuthoringError({ message: `query lowering: param ${name} is not in the query's registry` })
1714
2051
  }
1715
2052
  return id
1716
2053
  }
@@ -1718,13 +2055,13 @@ function paramIdOf(ctx: LowerContext, name: string): number {
1718
2055
  function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1719
2056
  const member = ctx.theory.relations[atom.relation.name]
1720
2057
  if (member !== atom.relation) {
1721
- throw errors.new(
1722
- `query lowering: relation ${atom.relation.name} is not the relation value schema ${ctx.theory.name} declares`
1723
- )
2058
+ throw new AuthoringError({
2059
+ message: `query lowering: relation ${atom.relation.name} is not the relation value schema ${ctx.theory.name} declares`
2060
+ })
1724
2061
  }
1725
2062
  const relationId = ctx.relationIds.get(atom.relation.name)
1726
2063
  if (relationId === undefined) {
1727
- throw errors.new(`query lowering: relation ${atom.relation.name} has no ordinal`)
2064
+ throw new AuthoringError({ message: `query lowering: relation ${atom.relation.name} has no ordinal` })
1728
2065
  }
1729
2066
  const ordered = sealedFieldsOf(atom.relation)
1730
2067
  const bindings: Array<readonly [number, TermIr]> = atom.bindings.map(function lowerBinding(binding) {
@@ -1732,7 +2069,9 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
1732
2069
  return candidate.name === binding.field
1733
2070
  })
1734
2071
  if (ordinal < 0) {
1735
- throw errors.new(`query lowering: relation ${atom.relation.name} has no field ${binding.field}`)
2072
+ throw new AuthoringError({
2073
+ message: `query lowering: relation ${atom.relation.name} has no field ${binding.field}`
2074
+ })
1736
2075
  }
1737
2076
  return [ordinal, lowerBindingTerm(ctx, `${atom.relation.name}.${binding.field}`, binding, ids)] as const
1738
2077
  })
@@ -1769,17 +2108,17 @@ function lowerInteriorAtom(
1769
2108
  ): AtomIr {
1770
2109
  const interior = ctx.interiorIds.get(target.name)
1771
2110
  if (interior === undefined) {
1772
- throw errors.new(`query lowering: derived table ${target.name} was not declared on this query`)
2111
+ throw new AuthoringError({ message: `query lowering: derived table ${target.name} was not declared on this query` })
1773
2112
  }
1774
2113
  if (target.finds.length === 0) {
1775
- throw errors.new(`query lowering: derived table ${target.name} has no head`)
2114
+ throw new AuthoringError({ message: `query lowering: derived table ${target.name} has no head` })
1776
2115
  }
1777
2116
  const irBindings: Array<readonly [number, TermIr]> = target.finds.map(function lowerPosition(column, position) {
1778
2117
  const binding = bindings.find(function byKey(candidate) {
1779
2118
  return candidate.key === column.name
1780
2119
  })
1781
2120
  if (binding === undefined) {
1782
- throw errors.new(`query lowering: interior ${target.name} omits head column ${column.name}`)
2121
+ throw new AuthoringError({ message: `query lowering: interior ${target.name} omits head column ${column.name}` })
1783
2122
  }
1784
2123
  return [position, { kind: "var", var: ids.of(binding.ref) } as const] as const
1785
2124
  })
@@ -1797,9 +2136,9 @@ function lowerCmpTerm(ctx: LowerContext, side: CmpTermData, sibling: CmpTermData
1797
2136
  case "literal": {
1798
2137
  const anchor = cmpAnchorOf(ctx, sibling)
1799
2138
  if (anchor === undefined) {
1800
- throw errors.new(
1801
- "query lowering: a comparison literal needs a bound-variable or anchored-param sibling to type it"
1802
- )
2139
+ throw new AuthoringError({
2140
+ message: "query lowering: a comparison literal needs a bound-variable or anchored-param sibling to type it"
2141
+ })
1803
2142
  }
1804
2143
  return { kind: "literal", value: taggedCmpLiteral("comparison literal", anchor, side.value, op) }
1805
2144
  }
@@ -1843,10 +2182,56 @@ function lowerCondition(ctx: LowerContext, cond: CondData, ids: VarIds): Conditi
1843
2182
  }
1844
2183
  }
1845
2184
 
2185
+ /**
2186
+ * Lowers one computed scalar node to the C05 wire (`#native.ts`'s
2187
+ * `ScalarExprIr`): the shared grammar arms pass through 1:1. Cached
2188
+ * construction depth is the bound; this walk emits wire only and does
2189
+ * not re-judge kinds (L14/native compile owns binding).
2190
+ */
2191
+ function lowerComputeNode(node: QueryNode, ids: VarIds): ScalarExprIr {
2192
+ if (node.depth > MAX_COMPUTE_DEPTH) {
2193
+ throw new AuthoringError({
2194
+ message: `query lowering: a compute expression is deeper than ${MAX_COMPUTE_DEPTH} nodes (the engine's scalar depth bound)`
2195
+ })
2196
+ }
2197
+ return lowerComputeGrammar(node, ids)
2198
+ }
2199
+
2200
+ function lowerComputeGrammar(node: QueryNode, ids: VarIds): ScalarExprIr {
2201
+ switch (node.kind) {
2202
+ case "var":
2203
+ return { kind: "var", var: ids.of(node.ref as AnyVar) }
2204
+ case "literal":
2205
+ return { kind: "literal", value: literalWireOf(node.value) }
2206
+ case "negate":
2207
+ case "isNaN":
2208
+ case "isFinite":
2209
+ return { kind: node.kind, expr: lowerComputeGrammar(node.expr, ids) }
2210
+ case "cast":
2211
+ return { kind: "cast", cast: node.cast, expr: lowerComputeGrammar(node.expr, ids) }
2212
+ case "add":
2213
+ case "subtract":
2214
+ case "multiply":
2215
+ case "divide":
2216
+ return {
2217
+ kind: node.kind,
2218
+ left: lowerComputeGrammar(node.left, ids),
2219
+ right: lowerComputeGrammar(node.right, ids)
2220
+ }
2221
+ default:
2222
+ throw new AuthoringError({
2223
+ message: "query lowering: a query-var tree cannot carry a source-field leaf — use Compute over bound variables"
2224
+ })
2225
+ }
2226
+ }
2227
+
1846
2228
  function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
1847
2229
  if (entry.kind === "var") {
1848
2230
  return { kind: "var", var: ids.of(entry.over) }
1849
2231
  }
2232
+ if (entry.kind === "compute") {
2233
+ return { kind: "compute", expr: lowerComputeNode(entry.expr, ids) }
2234
+ }
1850
2235
  const agg = entry.agg
1851
2236
  switch (agg.op) {
1852
2237
  case "count":
@@ -1874,6 +2259,9 @@ function headTermOf(column: FindColumn): HeadTermIr {
1874
2259
  if (entry.kind === "var") {
1875
2260
  return { kind: "var" }
1876
2261
  }
2262
+ if (entry.kind === "compute") {
2263
+ return { kind: "compute" }
2264
+ }
1877
2265
  return { kind: "aggregate", op: headOpOf(entry.agg) }
1878
2266
  }
1879
2267
 
@@ -1933,9 +2321,9 @@ function lowerQuery(q: AnyQuery): ParsedQuery {
1933
2321
  const params = new Map<string, ParamEntry>()
1934
2322
  q.data.params.forEach(function assignParamId(entry, index) {
1935
2323
  if (entry.anchor === undefined) {
1936
- throw errors.new(
1937
- `query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`
1938
- )
2324
+ throw new AuthoringError({
2325
+ message: `query param ${entry.name} has no field-anchored use — bind it in an atom or compare it against a bound variable`
2326
+ })
1939
2327
  }
1940
2328
  paramIds.set(entry.name, index)
1941
2329
  params.set(entry.name, entry)