@bjornpagen/bumbledb 0.7.0 → 0.9.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 (68) hide show
  1. package/COOKBOOK.md +105 -3
  2. package/README.md +3 -3
  3. package/dist/capacity.d.ts +310 -0
  4. package/dist/capacity.d.ts.map +1 -0
  5. package/dist/capacity.js +138 -0
  6. package/dist/capacity.js.map +1 -0
  7. package/dist/db.d.ts +5 -3
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +8 -8
  10. package/dist/db.js.map +1 -1
  11. package/dist/face.d.ts +1 -1
  12. package/dist/face.d.ts.map +1 -1
  13. package/dist/face.js.map +1 -1
  14. package/dist/index.d.ts +8 -8
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/law.d.ts +3 -2
  19. package/dist/law.d.ts.map +1 -1
  20. package/dist/law.js +1 -1
  21. package/dist/law.js.map +1 -1
  22. package/dist/lower.d.ts.map +1 -1
  23. package/dist/lower.js +3 -2
  24. package/dist/lower.js.map +1 -1
  25. package/dist/native.d.ts +6 -3
  26. package/dist/native.d.ts.map +1 -1
  27. package/dist/native.js.map +1 -1
  28. package/dist/order.d.ts +53 -2
  29. package/dist/order.d.ts.map +1 -1
  30. package/dist/order.js +30 -12
  31. package/dist/order.js.map +1 -1
  32. package/dist/query/atom.d.ts +33 -13
  33. package/dist/query/atom.d.ts.map +1 -1
  34. package/dist/query/atom.js +1 -1
  35. package/dist/query/atom.js.map +1 -1
  36. package/dist/query/find.d.ts +19 -8
  37. package/dist/query/find.d.ts.map +1 -1
  38. package/dist/query/find.js +6 -5
  39. package/dist/query/find.js.map +1 -1
  40. package/dist/schema.js +1 -1
  41. package/dist/schema.js.map +1 -1
  42. package/dist/spec.d.ts +76 -23
  43. package/dist/spec.d.ts.map +1 -1
  44. package/dist/spec.js +39 -7
  45. package/dist/spec.js.map +1 -1
  46. package/dist/statements.d.ts +42 -25
  47. package/dist/statements.d.ts.map +1 -1
  48. package/dist/statements.js +81 -19
  49. package/dist/statements.js.map +1 -1
  50. package/package.json +4 -4
  51. package/src/capacity.ts +454 -0
  52. package/src/db.ts +13 -11
  53. package/src/face.ts +1 -0
  54. package/src/index.ts +20 -9
  55. package/src/law.ts +3 -2
  56. package/src/lower.ts +3 -2
  57. package/src/native.ts +6 -3
  58. package/src/order.ts +83 -5
  59. package/src/query/atom.ts +37 -13
  60. package/src/query/find.ts +34 -16
  61. package/src/schema.ts +1 -1
  62. package/src/spec.ts +91 -25
  63. package/src/statements.ts +160 -34
  64. package/dist/count.d.ts +0 -102
  65. package/dist/count.d.ts.map +0 -1
  66. package/dist/count.js +0 -115
  67. package/dist/count.js.map +0 -1
  68. package/src/count.ts +0 -211
package/src/order.ts CHANGED
@@ -10,6 +10,16 @@
10
10
  * `by(...)` into one row-typed comparator. Cross-type cells cannot arise
11
11
  * within one column (one column, one domain), and the cell order is TOTAL
12
12
  * anyway (the type-rank wall), so the comparator never throws.
13
+ *
14
+ * ZERO keys is the identity key (ruled 2026-07-25): `by()` / `desc()` are
15
+ * the same two spellings over BARE scalar arrays (`bigint[]` of ids, map
16
+ * keys) — with no column to name, the value itself is the key, there is
17
+ * nothing to fold, and the comparator IS the result. One ordering
18
+ * vocabulary, no sibling names. The identity arms are typed to EXACTLY the
19
+ * engine-orderable roster ({@link EngineOrderable}) because a bare scalar
20
+ * carries no decoded-row provenance — the type wall must carry the
21
+ * orderability law itself, where the row arm's cells arrive under their
22
+ * columns' law-typed proof.
13
23
  */
14
24
 
15
25
  import type { FactValue } from "#native.ts"
@@ -23,6 +33,23 @@ interface Desc<K extends string> {
23
33
  /** One sort key: a bare column name (ascending — the punning spelling) or `desc(name)`. */
24
34
  type SortKey<K extends string> = K | Desc<K>
25
35
 
36
+ /**
37
+ * The engine-orderable scalar roster in the SDK's representation — `bigint`
38
+ * (U64/I64) and `boolean` (false < true — the strict 0/1 encoding IS the
39
+ * order), EXACTLY as `docs/architecture/10-data-model.md` § "Orderability,
40
+ * complete" admits it (ruled 2026-07-23, R3/R4), and nothing more:
41
+ * `string` is deliberately absent (intern ids are meaningless to order — a
42
+ * typed refusal, not a gap) and `number` is not an engine scalar at all.
43
+ * The identity comparators `by()`/`desc()` constrain on this alias, so
44
+ * sorting a `string[]` or `number[]` through them is a COMPILE error naming
45
+ * this roster. ONE OWNER for scalar ordering semantics: both identity arms
46
+ * route through the same cell order the row arm uses, whose bigint and
47
+ * boolean arms mirror the engine's U64/I64/Bool order exactly — a host
48
+ * sort over these values can never disagree with an engine order judgment
49
+ * (`Lt`-family, `Min`/`Max`) over the same column.
50
+ */
51
+ type EngineOrderable = bigint | boolean
52
+
26
53
  /**
27
54
  * The type-rank wall: boolean 0, bigint 1, string 2, bytes 3, interval 4.
28
55
  * One column carries one domain, so a mixed pair never arises from decoded
@@ -116,8 +143,38 @@ function cellCmp(left: FactValue, right: FactValue): number {
116
143
  return cellRank(left) - cellRank(right)
117
144
  }
118
145
 
119
- /** Marks one sort key DESCENDING — the single descending spelling (a bare name is already ascending). */
120
- function desc<const K extends string>(key: K): Desc<K> {
146
+ /**
147
+ * The ascending identity comparator `by()` returns — one value, minted
148
+ * once (`by() === by()`), {@link cellCmp} narrowed to the engine-orderable
149
+ * roster (the one owner of the cell order).
150
+ */
151
+ function identityAscending<T extends EngineOrderable>(left: T, right: T): number {
152
+ return cellCmp(left, right)
153
+ }
154
+
155
+ /** The descending identity comparator `desc()` returns — the same owner, sides flipped. */
156
+ function identityDescending<T extends EngineOrderable>(left: T, right: T): number {
157
+ return cellCmp(right, left)
158
+ }
159
+
160
+ /**
161
+ * The descending spelling — ONE name, both arities of one ordering
162
+ * vocabulary. `desc(name)` marks one sort key DESCENDING: plain data for
163
+ * {@link by} to fold (a bare name is already ascending). `desc()` — zero
164
+ * keys, the identity key (ruled 2026-07-25) — is the descending comparator
165
+ * over engine-orderable scalars themselves: a bare `bigint[]` or
166
+ * `boolean[]` has no column to name, so the value IS the key, there is
167
+ * nothing to fold, and the comparator is the result directly. The identity
168
+ * arm covers EXACTLY the {@link EngineOrderable} roster — `string` and
169
+ * `number` refuse at compile time, citing the orderability law
170
+ * (`10-data-model.md` § "Orderability, complete").
171
+ */
172
+ function desc(): <T extends EngineOrderable>(left: T, right: T) => number
173
+ function desc<const K extends string>(key: K): Desc<K>
174
+ function desc<const K extends string>(key?: K): Desc<K> | (<T extends EngineOrderable>(left: T, right: T) => number) {
175
+ if (key === undefined) {
176
+ return identityDescending
177
+ }
121
178
  const marker: Desc<K> = { key, desc: true }
122
179
  return Object.freeze(marker)
123
180
  }
@@ -130,12 +187,33 @@ function desc<const K extends string>(key: K): Desc<K> {
130
187
  * carries that proof here (parse-don't-validate). The generic RETURN is the
131
188
  * load-bearing trick: `rows.sort(by("rank"))` instantiates `Row` from the
132
189
  * array's own element type and checks the key set right there.
190
+ *
191
+ * ZERO keys is the identity key (ruled 2026-07-25): `by()` is the
192
+ * ascending comparator over engine-orderable scalars themselves —
193
+ * `ids.sort(by())` — for the bare arrays (`bigint[]`, map keys) the
194
+ * row-typed arm cannot reach because they have no column to name. One
195
+ * vocabulary, both arities. The identity arm is typed to EXACTLY the
196
+ * {@link EngineOrderable} roster: with no law-typed column standing proof
197
+ * over the values, the type wall carries the orderability law itself —
198
+ * `string` ordering is deliberately refused and `number` is not an engine
199
+ * scalar (`10-data-model.md` § "Orderability, complete"), so both refuse
200
+ * at compile time and a host sort can never disagree with an engine order
201
+ * judgment.
133
202
  */
203
+ function by(): <T extends EngineOrderable>(left: T, right: T) => number
134
204
  function by<const K extends string>(
135
205
  first: SortKey<K>,
136
206
  ...rest: ReadonlyArray<SortKey<K>>
137
- ): <Row extends Readonly<Record<K, FactValue>>>(left: Row, right: Row) => number {
138
- const entries = [first, ...rest].map(function normalizeKey(sortKey): { readonly key: K; readonly factor: 1 | -1 } {
207
+ ): <Row extends Readonly<Record<K, FactValue>>>(left: Row, right: Row) => number
208
+ function by<const K extends string>(
209
+ ...keys: ReadonlyArray<SortKey<K>>
210
+ ):
211
+ | (<T extends EngineOrderable>(left: T, right: T) => number)
212
+ | (<Row extends Readonly<Record<K, FactValue>>>(left: Row, right: Row) => number) {
213
+ if (keys.length === 0) {
214
+ return identityAscending
215
+ }
216
+ const entries = keys.map(function normalizeKey(sortKey): { readonly key: K; readonly factor: 1 | -1 } {
139
217
  if (typeof sortKey === "string") {
140
218
  return { key: sortKey, factor: 1 }
141
219
  }
@@ -152,5 +230,5 @@ function by<const K extends string>(
152
230
  }
153
231
  }
154
232
 
155
- export type { Desc, SortKey }
233
+ export type { Desc, EngineOrderable, SortKey }
156
234
  export { by, desc }
package/src/query/atom.ts CHANGED
@@ -335,8 +335,8 @@ type EqRight = AnyVar | Param<string> | SetParam<string> | bigint | string | boo
335
335
  /** What `ne`'s right side accepts. */
336
336
  type NeRight = AnyVar | Param<string> | bigint | string | boolean | Uint8Array | IntervalValue
337
337
 
338
- /** One side of an order comparison: orderable terms only (the IR's comparison rules). */
339
- type OrderSide = AnyVar | Param<string> | Duration | bigint
338
+ /** One side of an order comparison: orderable terms only (the IR's comparison rules — bool orders, `false < true`, R3). */
339
+ type OrderSide = AnyVar | Param<string> | Duration | bigint | boolean
340
340
 
341
341
  /** The point side of `pointIn`. */
342
342
  type PointSide = AnyVar | Param<string> | bigint
@@ -388,7 +388,7 @@ function order<Op extends "lt" | "le" | "gt" | "ge", const L extends OrderSide,
388
388
  return comparison(op, left, right, undefined)
389
389
  }
390
390
 
391
- /** Strict less-than (`ir::CmpOp::Lt`) — orderable sides only, never intervals/bytes/strings/bools. */
391
+ /** Strict less-than (`ir::CmpOp::Lt`) — orderable sides only (bool included: `false < true`, R3), never intervals/bytes/strings. */
392
392
  function lt<const L extends OrderSide, const R extends OrderSide>(left: L, right: R): Cmp<"lt", L, R> {
393
393
  return order("lt", left, right)
394
394
  }
@@ -531,19 +531,32 @@ function isRecTarget(value: MatchOwner | RecTarget): value is RecTarget {
531
531
  }
532
532
 
533
533
  /**
534
- * Whether a variable's OWN field is orderable (u64/i64). A CLOSED reference
534
+ * Whether a variable's OWN field is NUMERIC (u64/i64) the judgment the
535
+ * point side of `pointIn` and the `sum` input read: a point lives in the
536
+ * interval's element domain, and a quantifier is not an addition, so bool
537
+ * (orderable, never numeric) is exactly here refused. A CLOSED reference
535
538
  * is excluded even though its kind is `u64`: a vocabulary's declaration-id
536
539
  * order is an accident, not semantics (`docs/architecture/10-data-model.md`
537
540
  * § orderability), so every order-comparison and fold position refuses
538
- * closed-bound terms — this judgment is the one gate they all read, and the
539
- * construction-time validations in `#query/lower.ts` are its runtime twin.
541
+ * closed-bound terms — the construction-time validations in
542
+ * `#query/lower.ts` are that ban's runtime twin.
540
543
  */
541
- type OrderVarOk<V extends AnyVar> = V["field"] extends { readonly closed: ClosedRoster }
544
+ type NumericVarOk<V extends AnyVar> = V["field"] extends { readonly closed: ClosedRoster }
542
545
  ? false
543
546
  : V["field"]["kind"] extends "u64" | "i64"
544
547
  ? true
545
548
  : false
546
549
 
550
+ /**
551
+ * Whether a variable's OWN field is ORDERABLE (u64/i64/bool) — the one gate
552
+ * every order-comparison side, `min`/`max` input, and Arg key reads. Bool
553
+ * orders: `false < true`, the strict 0/1 encoding IS the order (ruled R3),
554
+ * exactly the engine's operand screen
555
+ * (`bumbledb/crates/bumbledb/src/ir/validate/context.rs`); bool has no
556
+ * closed variant, so the closed exclusion rides in {@link NumericVarOk}.
557
+ */
558
+ type OrderVarOk<V extends AnyVar> = V["field"]["kind"] extends "bool" ? true : NumericVarOk<V>
559
+
547
560
  /** Whether a variable's OWN field is interval-typed. */
548
561
  type IntervalVarOk<V extends AnyVar> = V["field"]["kind"] extends "interval" ? true : false
549
562
 
@@ -554,8 +567,8 @@ type OrderSideOk<T> = T extends AnyVar
554
567
  ? IntervalVarOk<V>
555
568
  : true
556
569
 
557
- /** One point side's judgment. */
558
- type PointSideOk<T> = T extends AnyVar ? OrderVarOk<T> : true
570
+ /** One point side's judgment (numeric only — a point lives in the interval's element domain, never bool). */
571
+ type PointSideOk<T> = T extends AnyVar ? NumericVarOk<T> : true
559
572
 
560
573
  /** One interval side's judgment. */
561
574
  type IntervalSideOk<T> = T extends AnyVar ? IntervalVarOk<T> : true
@@ -677,8 +690,18 @@ type EqParams<L, R> = L extends AnyVar
677
690
  : never
678
691
  : never
679
692
 
680
- /** An order side's params contribution (order params are always `bigint`). */
681
- type OrderSideParams<T> = T extends Param<infer P extends string> ? { readonly [Q in P]: bigint } : never
693
+ /**
694
+ * An order side's params contribution, typed by the SIBLING side the
695
+ * runtime anchors a param to its sibling variable's field, so a bool var
696
+ * orders against a `boolean` param; every var-less pairing stays `bigint`.
697
+ */
698
+ type OrderSideParams<T, Sib> =
699
+ T extends Param<infer P extends string>
700
+ ? { readonly [Q in P]: Sib extends AnyVar ? Infer<Sib["field"]> : bigint }
701
+ : never
702
+
703
+ /** The point side's params contribution (a point is always `bigint`). */
704
+ type PointParams<T> = T extends Param<infer P extends string> ? { readonly [Q in P]: bigint } : never
682
705
 
683
706
  /** An interval side's params contribution. */
684
707
  type IntervalSideParams<T> = T extends Param<infer P extends string> ? { readonly [Q in P]: IntervalValue } : never
@@ -697,9 +720,9 @@ type CondParams<C> = [AnyTreeChild] extends [C]
697
720
  ? Op extends "eq" | "ne"
698
721
  ? EqParams<L, R>
699
722
  : Op extends "lt" | "le" | "gt" | "ge"
700
- ? OrderSideParams<L> | OrderSideParams<R>
723
+ ? OrderSideParams<L, R> | OrderSideParams<R, L>
701
724
  : Op extends "pointIn"
702
- ? IntervalSideParams<L> | OrderSideParams<R>
725
+ ? IntervalSideParams<L> | PointParams<R>
703
726
  : Op extends "allen"
704
727
  ? IntervalSideParams<L> | IntervalSideParams<R> | MaskParams<M>
705
728
  : never
@@ -749,6 +772,7 @@ export type {
749
772
  MatchShape,
750
773
  NotAtom,
751
774
  NotIdbAtom,
775
+ NumericVarOk,
752
776
  OrderSide,
753
777
  OrderVarOk,
754
778
  ParamUse,
package/src/query/find.ts CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  import type { Infer } from "#fields.ts"
23
23
  import type { SchemaClasses } from "#law.ts"
24
- import type { IntervalVarOk, OrderVarOk } from "#query/atom.ts"
24
+ import type { IntervalVarOk, NumericVarOk, OrderVarOk } from "#query/atom.ts"
25
25
  import type { AnyVar, Duration, MintSlotOf } from "#query/scope.ts"
26
26
 
27
27
  /** One aggregate operator name of the find vocabulary. */
@@ -71,20 +71,21 @@ function countDistinct<const V extends AnyVar>(over: V): Agg<"countDistinct", V>
71
71
  }
72
72
 
73
73
  /**
74
- * Exact checked sum over an orderable (u64/i64) variable — wide
75
- * accumulator, one finalize range check; overflow is the engine's typed
76
- * runtime error — or over the measure (`r.sum(r.duration(w))`).
74
+ * Exact checked sum over a NUMERIC (u64/i64) variable — wide accumulator,
75
+ * one finalize range check; overflow is the engine's typed runtime error —
76
+ * or over the measure (`r.sum(r.duration(w))`). Bool stays refused: a
77
+ * quantifier is not an addition (R3).
77
78
  */
78
79
  function sum<const O extends AnyVar | Duration>(over: O): Agg<"sum", O> {
79
80
  return aggregate("sum", over, undefined)
80
81
  }
81
82
 
82
- /** Minimum over an orderable variable or the measure (orderable types only). */
83
+ /** Minimum over an orderable variable (u64/i64/bool — over bool, `Min` is the ALL quantifier; R3) or the measure. */
83
84
  function min<const O extends AnyVar | Duration>(over: O): Agg<"min", O> {
84
85
  return aggregate("min", over, undefined)
85
86
  }
86
87
 
87
- /** Maximum over an orderable variable or the measure (orderable types only). */
88
+ /** Maximum over an orderable variable (u64/i64/bool — over bool, `Max` is the ANY quantifier; R3) or the measure. */
88
89
  function max<const O extends AnyVar | Duration>(over: O): Agg<"max", O> {
89
90
  return aggregate("max", over, undefined)
90
91
  }
@@ -116,7 +117,22 @@ function pack<const V extends AnyVar>(over: V): Agg<"pack", V> {
116
117
  return aggregate("pack", over, undefined)
117
118
  }
118
119
 
119
- /** A fold input's judgment: an orderable variable, or the measure of an interval variable. */
120
+ /**
121
+ * A `sum` input's judgment: a NUMERIC variable — Sum over bool stays
122
+ * refused, a quantifier is not an addition (R3) — or the measure of an
123
+ * interval variable.
124
+ */
125
+ type SumOverOk<O> = O extends AnyVar
126
+ ? NumericVarOk<O>
127
+ : O extends Duration<infer V extends AnyVar>
128
+ ? IntervalVarOk<V>
129
+ : false
130
+
131
+ /**
132
+ * A `min`/`max` input's judgment: an ORDERABLE variable — bool folds:
133
+ * `Max` is Any and `Min` is All, the two quantifiers as the 0/1 encoding's
134
+ * extremes (R3) — or the measure of an interval variable.
135
+ */
120
136
  type FoldOverOk<O> = O extends AnyVar
121
137
  ? OrderVarOk<O>
122
138
  : O extends Duration<infer V extends AnyVar>
@@ -136,15 +152,17 @@ type FindEntryOk<E> = E extends AnyVar
136
152
  ? true
137
153
  : E extends Agg<"countDistinct", AnyVar>
138
154
  ? true
139
- : E extends Agg<"sum" | "min" | "max", infer O>
140
- ? FoldOverOk<O>
141
- : E extends Agg<"argMax" | "argMin", AnyVar, infer K extends AnyVar>
142
- ? OrderVarOk<K> extends true
143
- ? true
144
- : false
145
- : E extends Agg<"pack", infer V extends AnyVar>
146
- ? IntervalVarOk<V>
147
- : false
155
+ : E extends Agg<"sum", infer O>
156
+ ? SumOverOk<O>
157
+ : E extends Agg<"min" | "max", infer O>
158
+ ? FoldOverOk<O>
159
+ : E extends Agg<"argMax" | "argMin", AnyVar, infer K extends AnyVar>
160
+ ? OrderVarOk<K> extends true
161
+ ? true
162
+ : false
163
+ : E extends Agg<"pack", infer V extends AnyVar>
164
+ ? IntervalVarOk<V>
165
+ : false
148
166
 
149
167
  /** The validated find record (intersect with the inferred entries — errors land on the offending key). */
150
168
  type CheckFind<F extends FindShape> = {
package/src/schema.ts CHANGED
@@ -306,7 +306,7 @@ function schema<const Rels extends SchemaRelations, const Stmts extends readonly
306
306
  */
307
307
  if (!isStatement(statement)) {
308
308
  throw errors.new(
309
- `schema ${name}: a statement is minted only by key/contained/mirrors/window — a structural literal skips the construction-time arity and roster walls`
309
+ `schema ${name}: a statement is minted only by key/contained/mirrors/capacity — a structural literal skips the construction-time arity and roster walls`
310
310
  )
311
311
  }
312
312
  const rendered = renderStatement(statement)
package/src/spec.ts CHANGED
@@ -72,10 +72,10 @@ type LiteralSetSpec =
72
72
  | { readonly kind: "many"; readonly literals: readonly LiteralSpec[] }
73
73
 
74
74
  /**
75
- * One side of a containment or window: `R(fields… | field == literal…)`,
76
- * all names. `projection` is π in the statement's written order (positional
77
- * pairing with the other side); `selection` is σ as (field, literal-or-set)
78
- * pairs, read conjunctively.
75
+ * One side of a containment or capacity statement:
76
+ * `R(fields… | field == literal…)`, all names. `projection` is π in the
77
+ * statement's written order (positional pairing with the other side);
78
+ * `selection` is σ as (field, literal-or-set) pairs, read conjunctively.
79
79
  */
80
80
  interface SideSpec {
81
81
  readonly relation: string
@@ -84,16 +84,43 @@ interface SideSpec {
84
84
  }
85
85
 
86
86
  /**
87
- * A cardinality window's bounds the canonical-utterance law's surviving
88
- * spellings only, since the SDK's `Count` constructors make every banned
89
- * spelling unwritable or a construction error: `exact` is `{n}` (`{0}` the
90
- * exclusion), `range` is `{lo..hi}` with lo < hi, `floor` is `{lo..*}` with
91
- * lo 2.
87
+ * One capacity bound: a non-negative literal, a u64 field of the TARGET
88
+ * row (the dependent bound per-group capacity read at judge time), or
89
+ * the interval-measure of a TARGET-row field (`Duration(span)`). Names,
90
+ * not ids the spec is the name-level wire; the engine resolves bound
91
+ * names against the target's FULL roster (C1), never the projection.
92
92
  */
93
- type WindowSpec =
94
- | { readonly kind: "exact"; readonly n: bigint }
95
- | { readonly kind: "range"; readonly lo: bigint; readonly hi: bigint }
96
- | { readonly kind: "floor"; readonly lo: bigint }
93
+ type CapacityBoundSpec =
94
+ | { readonly kind: "lit"; readonly value: bigint }
95
+ | { readonly kind: "field"; readonly field: string }
96
+ | { readonly kind: "durationField"; readonly field: string }
97
+
98
+ /**
99
+ * A capacity statement's weight — a TOTAL sum (C4: `unit` is a case, not
100
+ * an absence): the count instance (`unit`), a u64 field of the SOURCE row
101
+ * (`field`), or a SOURCE-row interval's measure (`durationField`). The
102
+ * wire always carries it — a unit statement crosses as `{ kind: "unit" }`,
103
+ * never by omission.
104
+ */
105
+ type WeightSpec =
106
+ | { readonly kind: "unit" }
107
+ | { readonly kind: "field"; readonly field: string }
108
+ | { readonly kind: "durationField"; readonly field: string }
109
+
110
+ /**
111
+ * A capacity statement's window — the canonical-utterance law's surviving
112
+ * spellings, per-aggregate where weight-sensitive (design § 6), since the
113
+ * SDK's `within()` mint makes every banned spelling unwritable or a
114
+ * construction error: `exact` is `{n}` (`{0}` the exclusion on the unit
115
+ * instance, "total is zero" on a weighted one), `range` is `{lo..hi}` with
116
+ * lo < hi (`{0..hi}` the canonical ceiling; the hi slot admits a dependent
117
+ * bound — C6: hi only), `floor` is `{lo..*}` (`{1..*}` legal on weighted
118
+ * statements only).
119
+ */
120
+ type CapacityWindowSpec =
121
+ | { readonly kind: "exact"; readonly n: CapacityBoundSpec }
122
+ | { readonly kind: "range"; readonly lo: CapacityBoundSpec; readonly hi: CapacityBoundSpec }
123
+ | { readonly kind: "floor"; readonly lo: CapacityBoundSpec }
97
124
 
98
125
  /**
99
126
  * One field: name, structural type, host newtype name — the field's
@@ -148,8 +175,10 @@ interface RelationSpec {
148
175
  * One dependency statement, tagged by form. `==` is not a variant: a
149
176
  * bidirectional containment is `containment` with `bidirectional: true`,
150
177
  * lowered by the engine to the two adjacent containments (`source <=
151
- * target` first). `cardinality` is B-family, target-left: the target is the
152
- * per-group parent, the source is counted.
178
+ * target` first). `capacity` reads as the operator does (C2 — target,
179
+ * weight, window, source): the target is the per-group parent, the source
180
+ * is the weighed side, and the weight is ALWAYS present (`unit` the count
181
+ * instance).
153
182
  */
154
183
  type StatementSpec =
155
184
  | { readonly kind: "fd"; readonly relation: string; readonly projection: readonly string[] }
@@ -160,9 +189,10 @@ type StatementSpec =
160
189
  readonly bidirectional: boolean
161
190
  }
162
191
  | {
163
- readonly kind: "cardinality"
192
+ readonly kind: "capacity"
164
193
  readonly target: SideSpec
165
- readonly window: WindowSpec
194
+ readonly weight: WeightSpec
195
+ readonly window: CapacityWindowSpec
166
196
  readonly source: SideSpec
167
197
  }
168
198
 
@@ -308,22 +338,58 @@ function renderLiteralSet(set: LiteralSetSpec): string {
308
338
  }
309
339
 
310
340
  /**
311
- * Renders window bounds in their one canonical spelling: `{n}` exact
341
+ * Renders one capacity bound in its one canonical spelling: a literal as
342
+ * digits, a dependent bound bare by field name, an interval-measure bound
343
+ * as `Duration(field)` — the spellings the engine's renderer emits.
344
+ */
345
+ function renderCapacityBound(bound: CapacityBoundSpec): string {
346
+ switch (bound.kind) {
347
+ case "lit":
348
+ return bound.value.toString()
349
+ case "field":
350
+ return bound.field
351
+ case "durationField":
352
+ return `Duration(${bound.field})`
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Renders a capacity window in its one canonical spelling: `{n}` exact
312
358
  * (`{0}` the exclusion), `{lo..hi}`, `{lo..*}` — the spelling set the
313
- * engine's renderer emits for sealed statements.
359
+ * engine's renderer emits for sealed statements, bounds through
360
+ * {@link renderCapacityBound}.
314
361
  */
315
- function renderWindow(window: WindowSpec): string {
362
+ function renderCapacityWindow(window: CapacityWindowSpec): string {
316
363
  switch (window.kind) {
317
364
  case "exact":
318
- return `{${window.n}}`
365
+ return `{${renderCapacityBound(window.n)}}`
319
366
  case "range":
320
- return `{${window.lo}..${window.hi}}`
367
+ return `{${renderCapacityBound(window.lo)}..${renderCapacityBound(window.hi)}}`
321
368
  case "floor":
322
- return `{${window.lo}..*}`
369
+ return `{${renderCapacityBound(window.lo)}..*}`
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Renders a capacity weight as the operator's bracket: the unit weight
375
+ * renders NOTHING — the count utterance `<={lo..hi}` falls out of the one
376
+ * printer, never a second "legacy" arm — a field weight as `[field]`, an
377
+ * interval measure as `[Duration(field)]`.
378
+ */
379
+ function renderWeight(weight: WeightSpec): string {
380
+ switch (weight.kind) {
381
+ case "unit":
382
+ return ""
383
+ case "field":
384
+ return `[${weight.field}]`
385
+ case "durationField":
386
+ return `[Duration(${weight.field})]`
323
387
  }
324
388
  }
325
389
 
326
390
  export type {
391
+ CapacityBoundSpec,
392
+ CapacityWindowSpec,
327
393
  ClosedSpec,
328
394
  FieldSpec,
329
395
  LiteralSetSpec,
@@ -335,6 +401,6 @@ export type {
335
401
  StatementSpec,
336
402
  ValueSpec,
337
403
  ValueTypeSpec,
338
- WindowSpec
404
+ WeightSpec
339
405
  }
340
- export { renderLiteral, renderLiteralSet, renderWindow }
406
+ export { renderCapacityBound, renderCapacityWindow, renderLiteral, renderLiteralSet, renderWeight }