@bjornpagen/bumbledb 0.20.3 → 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
package/COOKBOOK.md CHANGED
@@ -1,1729 +1,401 @@
1
- # The cookbook modeling intuition as schemas, in TypeScript
1
+ # The Bumbledb TypeScript cookbook
2
2
 
3
- The bumbledb engine's 32 cookbook recipes (`bumbledb/docs/cookbook.md`),
4
- translated to this SDK's structural API. **This document is illustrative,
5
- never normative**: the compiled tests are the authority — every block here
6
- is executed by the sync suites — and the SDK is the same theory in another
7
- skin.
3
+ Worked recipes for the Effect-native surface: typed schema values,
4
+ declared keys and laws, application-owned `Uuid` identity, one bounded
5
+ native runtime, scoped resources, immutable final-state changes, `Option`
6
+ reads, sealed complete results, and one-shot page streams.
8
7
 
9
- Every `ts` fence below compiles VERBATIM against the current SDK —
10
- `ts/test/cookbook-doc.test.ts` extracts each recipe's fences from this
11
- document mechanically at test time and type-checks them against
12
- `src/index.ts` (the TS twin of the Rust cookbook's doc-sync pin), so an
13
- edit here whose code stops compiling fails the build. The runtime half of
14
- the claim is `ts/test/cookbook.test.ts`'s, over compiled copies of the
15
- recipes: each schema is constructed through the public surface, admitted
16
- on a real store (the engine's schema validation is the acceptance
17
- judgment), its fingerprint asserted stable across a reopen AND equal to
18
- the per-recipe cross-host golden the Rust cookbook suite also pins
19
- (`fixtures/cookbook-fingerprints.txt` at the repository root — the two
20
- cookbooks teach one
21
- theory per recipe number, provably), and every query snippet lowered
22
- through `db.prepare` (the engine's own IR validation).
23
-
24
- Guarantee labels that name Lean results cite the checked spec in the engine
25
- repo's `lean/` by theorem name, exactly as the engine cookbook does
26
- (`scripts/spec-census.sh` verifies every citation resolves); the label always
27
- names any additional Rust premise.
28
-
29
- Everything below imports from the one package entry:
8
+ Every `ts` fence below is extracted and type-checked against the real
9
+ package surface by `test/cookbook-doc.test.ts`; the imports fence here is
10
+ prepended to every recipe. The examples are lazy Effect programs — nothing
11
+ below runs a database at import time, and measured policies are inputs.
30
12
 
31
13
  ```ts
14
+ import { Effect, Option, Result, Stream } from "effect"
32
15
  import {
33
- ALLEN,
34
- Db,
35
- type Infer,
36
- abandon,
37
- bool,
38
- bytes,
39
16
  capacity,
17
+ ChangeSet,
40
18
  closed,
41
19
  contained,
20
+ Db,
42
21
  duration,
22
+ f64,
43
23
  i64,
24
+ uuid,
25
+ Uuid,
44
26
  interval,
45
27
  key,
46
28
  mirrors,
29
+ NativeRuntime,
47
30
  on,
48
31
  query,
49
32
  ref,
50
33
  relation,
34
+ Scalar,
51
35
  schema,
36
+ span,
52
37
  str,
53
38
  u64,
54
39
  v,
55
40
  weigh,
56
41
  within
57
42
  } from "@bjornpagen/bumbledb"
58
- ```
59
-
60
- ## The class laws — the statements type the columns
61
-
62
- Relation declarations are pure structure — `kind`, `width`, `element`,
63
- `fresh`, nothing else. Domains are never declared: `schema()` computes every
64
- field's equivalence class FROM the statement list, by union-find across every
65
- paired face (containment, `mirrors`, capacity targets, ψ-selected faces
66
- included). Three laws govern the classes:
67
-
68
- 1. **A fresh field is a generator** and names its class by its declaration
69
- coordinate (`"Attempt.id"`); a closed relation's id is a generator named
70
- the same way (`"Kind.id"`).
71
- 2. **A generator-less class** is named by its least member coordinate in
72
- relation-declaration × field-declaration order (recipe 5's
73
- `"Document.payload"`), deterministic and pinned forever.
74
- 3. **A field in no law is bare, and bare pairs only with bare** in queries —
75
- a deliberate sum-domain pointer stays legal because you simply write no
76
- law over it.
77
-
78
- The wall: at most ONE generator per class — a statement list that unifies two
79
- fresh coordinates is a contradiction (two mints cannot share a carrier), a
80
- schema-level compile error with a construction-time runtime twin. The
81
- dividend is that the statements you already write ARE the typing: a query
82
- variable first bound in one class refuses to join a field of another (compile
83
- error, with the same refusal thrown at construction), and no label, link, or
84
- domain declaration exists anywhere on the surface. When a recipe below says a
85
- statement "types" a column, this machinery is what it means. Classes never
86
- touch the fingerprint — identity hashes canonical descriptor bytes, never the
87
- law-computed names.
88
-
89
- ## Foundations
90
-
91
- ## 1. The minimal interval schema
92
-
93
- Guarantee: Lean theorem + validator/runtime premise — the pointwise key
94
- enforces per-service disjointness (`lean/Bumbledb/Dependencies.lean:
95
- pointwise_key_disjoint`); checked intervals supply nonempty values
96
- (`lean/Bumbledb/Values.lean: interval_nonempty`).
97
-
98
- One fact per outage window; the pointwise key is the whole temporal design.
99
-
100
- ```ts
101
- const Service = relation("Service", { id: u64.fresh, name: str })
102
- // The window is one value, not a (start, end) column pair: the denotation
103
- // (a set of points, half-open) is what the judgments below read through.
104
- // The containment below is what types `service` — the class laws.
105
- const Outage = relation("Outage", { service: u64, window: interval(i64) })
106
-
107
- const Uptime = schema("Uptime", { Service, Outage }, [
108
- contained(on(Outage, "service"), on(Service, "id")),
109
- // The pointwise key: per service, no two outages share a point — every
110
- // pair satisfies DISJOINT. SQL:2011's WITHOUT OVERLAPS, as a theorem.
111
- // `key(R, [...])` is the host flavor of the canonical arrow — this one
112
- // renders `Outage(service, window) -> Outage`: the key projection
113
- // determines the tuple, and the arrow closing over its own relation is
114
- // what makes a key a key (the compile-pin asserts the render).
115
- key(Outage, ["service", "window"])
116
- ])
117
-
118
- // down at instant t — v(Outage) mints a fresh variable per column (typed by
119
- // that column's law-class); destructure what you bind, reuse to join:
120
- const downAt = query(Uptime).rule((r) => {
121
- const { service, window } = v(Outage)
122
- return r
123
- .match(Outage, { service, window })
124
- .where(r.pointIn(r.param("t"), window))
125
- .find({ service })
126
- })
127
- // overlapping an incident window (one Allen mask, no operator zoo):
128
- const overlapping = query(Uptime).rule((r) => {
129
- const { service, window } = v(Outage)
130
- return r
131
- .match(Outage, { service, window })
132
- .where(r.allen(window, ALLEN.intersects, r.param("incident")))
133
- .find({ service, window })
134
- })
135
- // total downtime per service is host arithmetic on the interval
136
- // endpoints every answer row already carries (`end − start`).
137
- ```
138
-
139
- ## 2. Discriminated unions
140
-
141
- Guarantee: Lean theorem + validator/runtime premises — key-backed equality
142
- gives unique source/target correspondence (`lean/Bumbledb/Dependencies.lean:
143
- keyed_eq_unique_correspondence`); both projections must resolve to declared keys.
144
-
145
- Sum-typed entities: a closed-relation discriminator plus per-arm child
146
- relations, glued by bidirectional conditional containments.
147
-
148
- ```ts
149
- // The discriminator vocabulary is a closed relation: its ground axioms are
150
- // axioms, and a handle is its NAME — the string literal "Deterministic" is
151
- // the ONE spelling, on every surface (statements, inserts, queries, rows).
152
- const Kind = closed("Kind", ["Deterministic", "CustomOperator"])
153
- const Task = relation("Task", { id: u64.fresh, kind: Kind.id })
154
- const DeterministicGrading = relation("DeterministicGrading", { task: u64, tolerance: i64 })
155
- const CustomOperatorGrading = relation("CustomOperatorGrading", { task: u64, operator: str })
156
-
157
- const Grading = schema("Grading", { Kind, Task, DeterministicGrading, CustomOperatorGrading }, [
158
- contained(on(Task, "kind"), on(Kind, "id")), // the discriminator resolves
159
- key(DeterministicGrading, ["task"]), // one arm fact per parent
160
- key(CustomOperatorGrading, ["task"]),
161
- // Totality (==, left to right): a Deterministic task HAS its arm fact —
162
- // same commit, always. Arm validity (right to left): an arm fact's parent
163
- // exists WITH that kind — composite-FK-plus-CHECK, one statement. These
164
- // mirrors are also what type `task` on both arms: each lands in the
165
- // "Task.id" generator class.
166
- mirrors(on(Task.where({ kind: "Deterministic" }), "id"), on(DeterministicGrading, "task")),
167
- mirrors(on(Task.where({ kind: "CustomOperator" }), "id"), on(CustomOperatorGrading, "task"))
168
- // Exclusivity is a theorem, not a statement: one id in two arms would
169
- // force `kind` to equal two handles against the fresh key on id.
170
- // The executor spends the same theorem again as a diagnostic witness
171
- // (recipe 22); multi-rule execution still keeps a spanning seen-set.
172
- ])
173
-
174
- // Host dispatch over the discriminator is native `switch` narrowing over
175
- // the handle union (`Infer<typeof Kind.id>` = "Deterministic" |
176
- // "CustomOperator") — rows already arrive carrying the handle name, and
177
- // `satisfies never` makes the switch exhaustive: a missing arm is a
178
- // compile error.
179
- const gradedBy = (kind: Infer<typeof Kind.id>) => {
180
- switch (kind) {
181
- case "Deterministic":
182
- return "tolerance"
183
- case "CustomOperator":
184
- return "operator"
185
- default:
186
- return kind satisfies never
187
- }
188
- }
189
- ```
190
-
191
- ## 3. 0..1 optional attributes
192
-
193
- Guarantee: Lean theorem + validator/runtime premises — the child key proves at
194
- most one fact (`lean/Bumbledb/Dependencies.lean: functionality_unique_witness`)
195
- and containment requires its parent (`lean/Bumbledb/Dependencies.lean:
196
- contains_iff_view_subset`); absence remains legal.
197
-
198
- No nulls, anywhere. Optional data is an absent fact in a child relation; the
199
- child's key plus a one-way containment *is* "nullable column", done honestly.
200
-
201
- ```ts
202
- const Business = relation("Business", { id: u64.fresh, name: str })
203
- const MailingAddress = relation("MailingAddress", { business: u64, line: str, city: str })
204
-
205
- const Optionality = schema("Optionality", { Business, MailingAddress }, [
206
- key(MailingAddress, ["business"]), // at most one address...
207
- contained(on(MailingAddress, "business"), on(Business, "id")) // ...and only for a real business
208
- // One-way containment on purpose: absence is the fact that isn't. The
209
- // all-or-nothing column group (line+city together or neither) is
210
- // unstatable TO VIOLATE — the fact carries both fields or does not exist.
211
- ])
212
-
213
- // Negation is plain anti-join (no null branch exists in any operator):
214
- const unaddressed = query(Optionality).rule((r) => {
215
- const { id: b } = v(Business)
216
- return r
217
- .match(Business, { id: b })
218
- .where(r.not(MailingAddress, { business: b }))
219
- .find({ b })
220
- })
221
- ```
222
-
223
- ## 4. Money
224
-
225
- Guarantee: host discipline + validator premises — fixed-point scale and
226
- currency grouping live in host domains; containments only resolve references.
227
-
228
- Fixed-point i64 minor units. Floats are permanently refused (the ledger);
229
- proration and FX are host arithmetic. `minor` sits in no law — bare, and bare
230
- pairs only with bare (the class laws), so a stray join against some other
231
- i64 column refuses unless a statement puts them in one class. Scale and
232
- currency intent are host discipline; the closed `currency` reference is what
233
- the schema CAN say, and says.
234
-
235
- ```ts
236
- const Currency = closed("Currency", ["Usd", "Eur", "Gbp"])
237
- const Account = relation("Account", { id: u64.fresh, name: str })
238
- const Posting = relation("Posting", {
239
- id: u64.fresh,
240
- account: u64,
241
- currency: Currency.id,
242
- minor: i64
243
- })
244
-
245
- const Money = schema("Money", { Currency, Account, Posting }, [
246
- contained(on(Posting, "account"), on(Account, "id")),
247
- contained(on(Posting, "currency"), on(Currency, "id"))
248
- ])
43
+ import type {
44
+ ApplyOutcome,
45
+ CompleteResult,
46
+ ExecutionPolicy,
47
+ Fact,
48
+ NativeRuntimeOptions,
49
+ QueryReader
50
+ } from "@bjornpagen/bumbledb"
249
51
 
250
- // Multi-currency totals: currency is a group key, never summed across —
251
- // Sum folds wide with one final range check, so totals cannot wrap
252
- // silently. Bind the fresh id: set semantics would collapse two equal
253
- // (account, currency, minor) postings without it.
254
- const totals = query(Money).rule((r) => {
255
- const { id, account, currency, minor } = v(Posting)
256
- return r.match(Posting, { id, account, currency, minor }).find({ account, currency, total: r.sum(minor) })
257
- })
52
+ declare const work: ExecutionPolicy
53
+ declare const runtimePolicy: NativeRuntimeOptions
54
+ declare const localPath: string
258
55
  ```
259
56
 
260
- ## 5. Content addressing
261
-
262
- Guarantee: validator/runtime premises + host discipline — the payload key and
263
- containments enforce identity/reference shape; hashing and blob durability stay external.
57
+ ## 1. One schema, typed twice — relations, declared keys, laws
264
58
 
265
- The decision rule for byte-shaped data: **intern what repeats (`str`); inline
266
- what identifies (`bytes(n)`)**.
59
+ Identity fields are ordinary application-owned `Uuid` values: the database
60
+ issues no identity and there is no `fresh` mint. Keys are declared
61
+ statements; references and capacity are laws over the same fields. The same
62
+ declarations, spelled in Rust's `schema!`, produce the same canonical schema
63
+ identity.
267
64
 
268
65
  ```ts
269
- const Region = closed("Region", ["Us", "Eu"])
270
- const Document = relation("Document", {
271
- id: u64.fresh,
272
- name: str, // repeats: interned, id-equality
273
- payload: bytes(32) // identifies: the blake3 of the external blob — inline, never interned
66
+ const Student = relation("Student", { id: uuid, name: str, budget: u64 })
67
+ const Attempt = relation("Attempt", {
68
+ id: uuid,
69
+ student: uuid,
70
+ score: f64,
71
+ units: u64,
72
+ active: interval(i64)
274
73
  })
275
- const Replica = relation("Replica", { payload: bytes(32), region: Region.id })
276
74
 
277
- const Content = schema("Content", { Region, Document, Replica }, [
278
- key(Document, ["payload"]), // content-addressed: one doc per digest
279
- // This containment is the digest columns' typing: neither is fresh, so
280
- // the pair is a generator-less class named by its least member
281
- // coordinate — "Document.payload" (the class laws' second rule).
282
- contained(on(Replica, "payload"), on(Document, "payload")),
283
- contained(on(Replica, "region"), on(Region, "id"))
284
- // bytes(n) is identity-only (Eq/Ne, membership): a digest's lexicographic
285
- // order is an encoding artifact, refused as semantics. Large objects:
286
- // facts stay fixed-width; the payload lives in external storage,
287
- // referenced by identity (the large-object refusal).
75
+ const Learning = schema("Learning", { Student, Attempt }, [
76
+ key(Student, ["id"]),
77
+ key(Attempt, ["id"]),
78
+ contained(on(Attempt, "student"), on(Student, "id")),
79
+ capacity(on(Student, "id"), {
80
+ from: on(Attempt, "student"),
81
+ weight: weigh("units"),
82
+ within: within(0n, ref("budget"))
83
+ })
288
84
  ])
289
85
 
290
- // a bytes param self-encodes (Uint8Array by inference):
291
- const byDigest = query(Content).rule((r) => {
292
- const { id } = v(Document)
293
- return r.match(Document, { id, payload: r.param("digest") }).find({ id })
294
- })
86
+ // Schema construction is pure metadata: the value is inert data, and typing
87
+ // follows it a Fact<typeof Attempt> is the inferred row object.
88
+ declare const row: Fact<typeof Attempt>
89
+ const scoreIsNumber: number = row.score
90
+ const unitsAreExact: bigint = row.units
91
+ void [Learning, scoreIsNumber, unitsAreExact]
295
92
  ```
296
93
 
297
- ## Vocabularies
298
-
299
- ## 6. The vocabulary
94
+ ## 2. One runtime layer; explicit create and open
300
95
 
301
- Guarantee: Lean theorem + validator/runtime premise — the sealed closed
302
- extension is constant at every instance (`lean/Bumbledb/Schema.lean:
303
- den_closed_constant`) and the compiled member-set containment admits only
304
- declared priority handles.
305
-
306
- The enum idiom's replacement, first-class: a vocabulary is a **closed
307
- relation** — its ground axioms are declared in the schema, sealed at
308
- validate, frozen by the fingerprint, virtual in storage. The store holds zero
309
- vocabulary bytes, and handle names are the string literals on every surface.
96
+ `NativeRuntime.layer(options)` acquires the single bounded native runtime
97
+ with scope; reuse ONE layer value so Effect's memoization shares it.
98
+ `Db.open` never creates a missing database; `Db.create` refuses existing
99
+ authority. Both are scoped acquisitions.
310
100
 
311
101
  ```ts
312
- // Tier 1: handles only. At the host surface a handle is its NAME — a string
313
- // literal of the roster's union ("Low" | "Normal" | "Urgent"); the engine's
314
- // vocabulary stays relational (ids = declaration order) and the marshal
315
- // owns the bijection. Dispatch is native `switch` narrowing (recipe 2).
316
- const Priority = closed("Priority", ["Low", "Normal", "Urgent"])
317
-
318
- const Ticket = relation("Ticket", { id: u64.fresh, priority: Priority.id, opened_at: i64 })
319
-
320
- const Tickets = schema("Tickets", { Priority, Ticket }, [
321
- // A closed reference is an ordinary u64 under one containment (which also
322
- // types `priority` into the "Priority.id" generator class); the judgment
323
- // compiles at validate to a member-set test — one AND, one bit test, no
324
- // probe (30-dependencies.md).
325
- contained(on(Ticket, "priority"), on(Priority, "id"))
326
- ])
327
-
328
- // Handles are literals in queries exactly as in statements, and the
329
- // renderer prints them back — the round trip runs on names. The boundary
330
- // law: intrinsic meaning goes here (changing it is a new theory); policy
331
- // that drifts without a rebuild is an ordinary relation — a vocabulary is
332
- // never written, only declared.
333
- const urgent = query(Tickets).rule((r) => {
334
- const { id: t } = v(Ticket)
335
- return r.match(Ticket, { id: t, priority: "Urgent" }).find({ t })
336
- })
337
-
338
- // Set membership is a plain array — the drizzle law's spelling, closed-only
339
- // in query match records (an ordinary u64/str field's membership is a bound
340
- // ∈-set param, `r.inSet`); the array folds to the same wire set the param
341
- // spelling crosses. In `.where()` selections arrays work at EVERY field kind.
342
- const actionable = query(Tickets).rule((r) => {
343
- const { id: t } = v(Ticket)
344
- return r.match(Ticket, { id: t, priority: ["Normal", "Urgent"] }).find({ t })
345
- })
346
- ```
347
-
348
- ## 7. The classification
102
+ const Doc = relation("Doc", { id: uuid, text: str })
103
+ const Docs = schema("Docs", { Doc }, [key(Doc, ["id"])])
349
104
 
350
- Guarantee: validator/runtime premise — closed payload facts and the
351
- ψ-selected containment restrict certificates to the compiled
352
- mastered-handle set.
353
-
354
- The fused form: the vocabulary carries its intrinsic facts as **payload
355
- columns** — one ground axiom per handle, values sealed with the schema, read
356
- by ψ-selections. Axioms are declared, never written.
357
-
358
- ```ts
359
- // Tier 2: payload columns state what each word MEANS, next to the word.
360
- // A rubric change is a new theory — exactly right for meaning.
361
- const Kind = closed(
362
- "Kind",
363
- ["DirectPass", "JudgedPass", "Failed"],
364
- { mastered: bool, rank: u64 },
365
- {
366
- DirectPass: { mastered: true, rank: 30n },
367
- JudgedPass: { mastered: true, rank: 20n },
368
- Failed: { mastered: false, rank: 10n }
369
- }
105
+ const openExisting = Effect.scoped(
106
+ Effect.gen(function* () {
107
+ const db = yield* Db.open(localPath, Docs, work)
108
+ return db.schemaId
109
+ })
370
110
  )
371
- const Attempt = relation("Attempt", { id: u64.fresh, kind: Kind.id })
372
- const Certificate = relation("Certificate", { attempt: u64, kind: Kind.id })
373
-
374
- const Review = schema("Review", { Kind, Attempt, Certificate }, [
375
- contained(on(Attempt, "kind"), on(Kind, "id")),
376
- key(Certificate, ["attempt"]),
377
- contained(on(Certificate, "attempt"), on(Attempt, "id")),
378
- // ψ reads the payload: certificates carry mastered kinds only — the
379
- // member set {DirectPass, JudgedPass} compiles at validate and the
380
- // judgment is O(1) at commit (recipe 8 is this statement's own recipe).
381
- contained(on(Certificate, "kind"), on(Kind.where({ mastered: true }), "id"))
382
- ])
383
111
 
384
- // The classification read duplicates no flag onto Attempt — ψ walks the
385
- // vocabulary's payload in the query too: a closed relation is matchable
386
- // exactly like an ordinary one, and the atom folds at prepare into a
387
- // plan-constant handle set on its sibling.
388
- const masteredAttempts = query(Review).rule((r) => {
389
- const { id: a, kind: k } = v(Attempt)
390
- return r
391
- .match(Attempt, { id: a, kind: k })
392
- .match(Kind, { id: k, mastered: true })
393
- .find({ a })
394
- })
395
-
396
- // Host dispatch on the payload tier is the record-table idiom — a `Record`
397
- // over the handle union is total by type (a missing or extra entry is a
398
- // compile error), and each entry reads its sealed axiom row off the typed
399
- // `Kind.axioms` readback:
400
- const labels: Record<Infer<typeof Kind.id>, string> = {
401
- DirectPass: `mastered, rank ${Kind.axioms.DirectPass.rank}`,
402
- JudgedPass: `mastered, rank ${Kind.axioms.JudgedPass.rank}`,
403
- Failed: "not mastered"
404
- }
405
- const label = (k: Infer<typeof Kind.id>) => labels[k]
406
- ```
407
-
408
- Two honest boundaries. The fold has limits: payload escaping to the head and
409
- param-bearing filters don't fold at prepare — the engine falls back to a
410
- virtual-image join over the sealed extension, and the semantics are identical
411
- either way. And for an ALREADY-DEPLOYED store, moving from the old
412
- complement-window workaround to these ψ spellings is a NEW theory — a
413
- different fingerprint — which is recipe 28's ETL territory: humans own that
414
- migration.
415
-
416
- ## 8. The sub-vocabulary
417
-
418
- Guarantee: validator/runtime premise — ψ over the sealed extension compiles
419
- the exact paging member set; a nonmember write is commit-rejected.
420
-
421
- The ψ-selected containment: a reference constrained to the facts of a
422
- vocabulary that satisfy a payload selection. Because the target is closed and
423
- sealed, the enforcement plan is not a probe strategy — it is **the answer set
424
- itself**, fixed when the schema is built.
425
-
426
- ```ts
427
- const Severity = closed(
428
- "Severity",
429
- ["Info", "Warning", "Critical", "Fatal"],
430
- { pages: bool },
431
- {
432
- Info: { pages: false },
433
- Warning: { pages: false },
434
- Critical: { pages: true },
435
- Fatal: { pages: true }
436
- }
112
+ const createOnce = Effect.scoped(
113
+ Effect.gen(function* () {
114
+ const db = yield* Db.create(localPath, Docs, work)
115
+ return db.schemaId
116
+ })
437
117
  )
438
- const Incident = relation("Incident", { id: u64.fresh, severity: Severity.id })
439
- const Escalation = relation("Escalation", { incident: u64, severity: Severity.id, at: i64 })
440
-
441
- const Oncall = schema("Oncall", { Severity, Incident, Escalation }, [
442
- contained(on(Incident, "severity"), on(Severity, "id")),
443
- contained(on(Escalation, "incident"), on(Incident, "id")),
444
- // The sub-vocabulary: an escalation carries a PAGING severity, by
445
- // statement. ψ over the sealed extension compiles to the member set
446
- // {Critical, Fatal}; the judgment is one bit test per touched fact,
447
- // and an escalation at "Info" aborts the commit.
448
- contained(on(Escalation, "severity"), on(Severity.where({ pages: true }), "id"))
449
- ])
450
-
451
- // who is being paged — the same ψ, on the read side:
452
- const paged = query(Oncall).rule((r) => {
453
- const { incident: i, severity: s } = v(Escalation)
454
- return r
455
- .match(Escalation, { incident: i, severity: s })
456
- .match(Severity, { id: s, pages: true })
457
- .find({ i })
458
- })
459
- ```
460
-
461
- ## Structure
462
-
463
- ## 9. Ordered collections
464
-
465
- Guarantee: Lean theorem + validator/runtime premises — mutual point coverage
466
- plus pointwise keys realizes exact partition
467
- (`lean/Bumbledb/Dependencies.lean: exact_partition_iff`), and the mixed-width
468
- interval positions type by element domain
469
- (`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); ordering the result
470
- remains a host presentation step — the SDK ships the comparator (`by`/`desc`,
471
- keys as data); the engine never orders.
472
-
473
- The linked-list verdict: successor pointers are control flow smuggled into
474
- data. Order is a value. The idiomatic ordered collection is an interval
475
- partition, spelled as a **triple**: the entity, the extent as a 0..1 child
476
- (empty lists exist, empty intervals do not — presence of the child IS
477
- nonemptiness), and the unit-slot sidecar (`interval(u64, 1n)` — the width is
478
- the type: a wrong-width value is unrepresentable).
479
-
480
- ```ts
481
- const Playlist = relation("Playlist", { id: u64.fresh, name: str })
482
- // The extent: a 0..1 child, because empty playlists exist and empty
483
- // intervals do not — presence of the child IS nonemptiness.
484
- const Extent = relation("Extent", { playlist: u64, span: interval(u64) })
485
- // The unit slot: position p occupies [p, p+1) — the width is the type.
486
- const Slot = relation("Slot", { playlist: u64, slot: interval(u64, 1n), track: str })
487
-
488
- const Playlists = schema("Playlists", { Playlist, Extent, Slot }, [
489
- contained(on(Extent, "playlist"), on(Playlist, "id")),
490
- contained(on(Slot, "playlist"), on(Playlist, "id")),
491
- key(Extent, ["playlist"]), // 0..1 extent per playlist
492
- key(Extent, ["playlist", "span"]), // exact target key (recipe 26's note)
493
- key(Slot, ["playlist", "slot"]), // one occupant per position
494
- mirrors(on(Extent, ["playlist", "span"]), on(Slot, ["playlist", "slot"])) // slots tile the span exactly
495
- ])
496
-
497
- // Positional access is membership — "what plays at position ?pos":
498
- const playingAt = query(Playlists).rule((r) => {
499
- const { slot, track } = v(Slot)
500
- return r
501
- .match(Slot, { playlist: r.param("list"), slot, track })
502
- .where(r.pointIn(r.param("pos"), slot))
503
- .find({ track })
504
- })
505
-
506
- // Answers are SETS — the host sorts them. Limit is the language's own
507
- // `.slice(0, n)`.
508
- const inPlayOrder = [
509
- { slot: { start: 1n, end: 2n }, track: "b" },
510
- { slot: { start: 0n, end: 1n }, track: "a" }
511
- ].sort(function bySlotThenTrack(left, right) {
512
- if (left.slot.start < right.slot.start) {
513
- return -1
514
- }
515
- if (left.slot.start > right.slot.start) {
516
- return 1
517
- }
518
- if (left.slot.end < right.slot.end) {
519
- return -1
520
- }
521
- if (left.slot.end > right.slot.end) {
522
- return 1
523
- }
524
- if (left.track < right.track) {
525
- return -1
526
- }
527
- if (left.track > right.track) {
528
- return 1
529
- }
530
- return 0
531
- })
532
- ```
533
-
534
- Middle insert is honest about its cost: making room at position `k` shifts
535
- every later slot and grows the extent — O(k) writes in **one delta**, judged
536
- once at commit. If middle inserts dominate, the demoted escape hatch is the
537
- spread slot: a scalar `pos: u64` written in gapped strides under the same
538
- composite key — bumbledb has no lexicographic fractional indexing, because
539
- string order is refused: there is no "between two strings" to allocate.
540
-
541
- ## 10. Trees and ASTs
542
-
543
- Guarantee: Lean theorem + validator/runtime premises for key-backed arms
544
- (`lean/Bumbledb/Dependencies.lean: keyed_eq_unique_correspondence`); host
545
- discipline for acyclicity — statements prove arm/edge shape, never a tree theorem.
546
118
 
547
- Node header + per-kind arms (recipe 2's pattern); every edge resolves; the
548
- shape theorems come from keys on the edge relations.
549
-
550
- ```ts
551
- const Kind = closed("Kind", ["Lit", "Add"])
552
- const Node = relation("Node", { id: u64.fresh, kind: Kind.id })
553
- const Lit = relation("Lit", { node: u64, value: i64 })
554
- const Add = relation("Add", { node: u64, lhs: u64, rhs: u64 })
555
- const Parent = relation("Parent", { child: u64, parent: u64 })
556
-
557
- const Ast = schema("Ast", { Kind, Node, Lit, Add, Parent }, [
558
- contained(on(Node, "kind"), on(Kind, "id")),
559
- key(Lit, ["node"]),
560
- key(Add, ["node"]),
561
- // Every node's arm is total, valid, and exclusive (recipe 2's theorems):
562
- mirrors(on(Node.where({ kind: "Lit" }), "id"), on(Lit, "node")),
563
- mirrors(on(Node.where({ kind: "Add" }), "id"), on(Add, "node")),
564
- // Every child edge resolves — no dangling subtrees, judged at commit
565
- // (these containments also put lhs/rhs in the "Node.id" class, which is
566
- // exactly what lets the query below join lhs against Lit.node):
567
- contained(on(Add, "lhs"), on(Node, "id")),
568
- contained(on(Add, "rhs"), on(Node, "id")),
569
- // Functional parent (one parent per child) ⇒ the reachable shape is
570
- // paths-or-cycles; acyclicity itself is outside the ∀∃ vocabulary
571
- // host discipline, recorded. Transitive reach is recipe 24's closure.
572
- key(Parent, ["child"]),
573
- contained(on(Parent, "child"), on(Node, "id")),
574
- contained(on(Parent, "parent"), on(Node, "id"))
575
- ])
576
-
577
- const lhsLiteral = query(Ast).rule((r) => {
578
- const { lhs: l } = v(Add)
579
- const { value } = v(Lit)
580
- return r
581
- .match(Add, { node: r.param("n"), lhs: l })
582
- .match(Lit, { node: l, value })
583
- .find({ value })
584
- })
119
+ // One boundary; an Effect app provides the layer in its own graph instead.
120
+ const layer = NativeRuntime.layer(runtimePolicy)
121
+ void [openExisting.pipe(Effect.provide(layer)), createOnce.pipe(Effect.provide(layer))]
122
+ ```
123
+
124
+ ## 3. Changes: build lazily, normalize once, apply atomically
125
+
126
+ `ChangeSet.builder` acquires a scoped database-free draft. Ingestion effects
127
+ are lazy and re-runnable while the draft is building — each execution reads
128
+ the then-current iterable and charges work again. Within ONE change set the
129
+ normalization is `(add, remove add)`: the identical fact's add wins
130
+ independent of call order. `finish()` consumes the draft into an immutable,
131
+ reusable `ChangeSet`.
132
+
133
+ ```ts
134
+ const Task = relation("Task", { id: uuid, title: str, done: u64 })
135
+ const Tasks = schema("Tasks", { Task }, [key(Task, ["id"])])
136
+
137
+ const applyOnce = Effect.scoped(
138
+ Effect.gen(function* () {
139
+ const db = yield* Db.open(localPath, Tasks, work)
140
+ const taskId = yield* Effect.sync(() => crypto.randomUUID())
141
+ const draft = yield* ChangeSet.builder(Tasks, work)
142
+ yield* draft.insert(Task, [{ id: taskId, title: "write the cookbook", done: 0n }])
143
+ const changes = yield* draft.finish()
144
+ const outcome: ApplyOutcome = yield* db.apply(changes, { ...work, expected: { kind: "any" } })
145
+ switch (outcome.kind) {
146
+ case "accepted":
147
+ case "no-change":
148
+ return outcome.witness
149
+ case "invariant-rejected":
150
+ // Complete statement diagnostics, typed data — never a throw.
151
+ return yield* Effect.fail(outcome.violations)
152
+ case "moved":
153
+ return outcome.current
154
+ }
155
+ })
156
+ )
157
+ void applyOnce
585
158
  ```
586
159
 
587
- ## 11. Typed graphs
588
-
589
- Guarantee: validator/runtime premises — endpoint containments type each edge
590
- and composite keys deduplicate pairs; no transitive graph property is claimed.
160
+ ## 4. Keyed reads are Options
591
161
 
592
- One relation per edge kind: endpoint containments pin which node kinds each
593
- edge may touch.
162
+ `get` reads through the relation's primary declared key. A missing key is
163
+ `Option.none` never a fake I/O error, never a nullable row.
594
164
 
595
165
  ```ts
596
- const Person = relation("Person", { id: u64.fresh, name: str })
597
- const Repo = relation("Repo", { id: u64.fresh, name: str })
598
- const Follows = relation("Follows", { follower: u64, followee: u64 })
599
- const Maintains = relation("Maintains", { person: u64, repo: u64 })
166
+ const Person = relation("Person", { id: uuid, name: str })
167
+ const People = schema("People", { Person }, [key(Person, ["id"])])
600
168
 
601
- const Graph = schema("Graph", { Person, Repo, Follows, Maintains }, [
602
- contained(on(Follows, "follower"), on(Person, "id")), // a Person→Person edge, by statement —
603
- contained(on(Follows, "followee"), on(Person, "id")), // a Follows fact cannot touch a Repo
604
- key(Follows, ["follower", "followee"]), // at most one edge per pair
605
- contained(on(Maintains, "person"), on(Person, "id")),
606
- contained(on(Maintains, "repo"), on(Repo, "id")),
607
- key(Maintains, ["person", "repo"])
608
- ])
609
-
610
- // Mutual follows — joins are explicit var reuse on both ends (both columns
611
- // live in the "Person.id" class, so the reuse is lawful); `lt` keeps each
612
- // pair once:
613
- const mutual = query(Graph).rule((r) => {
614
- const { follower: a, followee: b } = v(Follows)
615
- return r
616
- .match(Follows, { follower: a, followee: b })
617
- .match(Follows, { follower: b, followee: a })
618
- .where(r.lt(a, b))
619
- .find({ a, b })
620
- })
621
- ```
622
-
623
- ## 12. Entity-component
624
-
625
- Guarantee: definition + validator/runtime premises — component keys give 0..1
626
- and containments require the stated entity/archetype facts.
627
-
628
- The 0..1 idiom (recipe 3) at scale: components are sidecar relations; an
629
- entity has a component iff the fact exists; a new component kind is a new
630
- relation, not a wider fact.
631
-
632
- ```ts
633
- const Entity = relation("Entity", { id: u64.fresh, name: str })
634
- const Transform = relation("Transform", { entity: u64, x: i64, y: i64 })
635
- const Velocity = relation("Velocity", { entity: u64, dx: i64, dy: i64 })
636
- const Renderable = relation("Renderable", { entity: u64, mesh: str })
637
-
638
- const Ecs = schema("Ecs", { Entity, Transform, Velocity, Renderable }, [
639
- key(Transform, ["entity"]), // each component 0..1 per entity
640
- contained(on(Transform, "entity"), on(Entity, "id")),
641
- key(Velocity, ["entity"]),
642
- contained(on(Velocity, "entity"), on(Entity, "id")),
643
- key(Renderable, ["entity"]),
644
- // An archetype rule is one containment: every Renderable has a Transform
645
- // (and, through it, an Entity — containment composes, and the class
646
- // composes with it: every `entity` column lands in "Entity.id").
647
- contained(on(Renderable, "entity"), on(Transform, "entity"))
648
- ])
649
-
650
- // The physics join is the component intersection:
651
- const physics = query(Ecs).rule((r) => {
652
- const { entity, x, y } = v(Transform)
653
- const { dx, dy } = v(Velocity)
654
- return r
655
- .match(Transform, { entity, x, y })
656
- .match(Velocity, { entity, dx, dy })
657
- .find({ entity, x, y, dx, dy })
658
- })
169
+ const lookup = (reader: QueryReader<typeof People>, personId: Uuid) =>
170
+ Effect.gen(function* () {
171
+ const found = yield* reader.get(Person, { id: personId }, work)
172
+ return Option.isSome(found) ? found.value.name : "unknown"
173
+ })
174
+ void lookup
659
175
  ```
660
176
 
661
- ## 13. State machines
177
+ ## 5. Queries are reusable typed values
662
178
 
663
- Guarantee: Lean theorem + validator/runtime premises for the shipped arm
664
- (`lean/Bumbledb/Dependencies.lean: keyed_eq_unique_correspondence`); host
665
- discipline for allowed transitions equality pins state evidence, not paths.
666
-
667
- States are a discriminated union; per-state data lives in arms; and the
668
- conditional reference target — a reference to "an order *that is shipped*" —
669
- is one selected statement, the statement SQL cannot write.
179
+ `v(R)` mints typed variables; reusing one across `match` records is the
180
+ join. The `find` record names the answer columns, and parameters infer from
181
+ use. A template contains no tenant rows or live snapshot — execute it
182
+ against any matching-schema reader.
670
183
 
671
184
  ```ts
672
- const State = closed("State", ["Cart", "Placed", "Shipped"])
673
- const Order = relation("Order", { id: u64.fresh, state: State.id })
674
- const Placement = relation("Placement", { order: u64, at: i64 })
675
- const Shipment = relation("Shipment", { order: u64, carrier: str, at: i64 })
676
-
677
- const Orders = schema("Orders", { State, Order, Placement, Shipment }, [
678
- contained(on(Order, "state"), on(State, "id")),
679
- key(Placement, ["order"]),
680
- key(Shipment, ["order"]),
681
- // History accretes: a Shipped order keeps its Placement — one-way
682
- // containment admits arms from earlier states surviving the transition.
683
- contained(on(Placement, "order"), on(Order, "id")),
684
- // The conditional target, both ways: every Shipment references an order
685
- // THAT IS Shipped (validity), and every Shipped order has its Shipment
686
- // (totality) — the transition and its evidence commit together.
687
- mirrors(on(Shipment, "order"), on(Order.where({ state: "Shipped" }), "id"))
688
- // Transition predicates ("only Placed may ship") are host code under the
689
- // generation witness — recipe 20; the schema pins the states, not the paths.
185
+ const Author = relation("Author", { id: uuid, name: str })
186
+ const Book = relation("Book", { id: uuid, author: uuid, pages: u64 })
187
+ const Library = schema("Library", { Author, Book }, [
188
+ key(Author, ["id"]),
189
+ key(Book, ["id"]),
190
+ contained(on(Book, "author"), on(Author, "id"))
690
191
  ])
691
192
 
692
- const shipped = query(Orders).rule((r) => {
693
- const { id } = v(Order)
694
- const { carrier } = v(Shipment)
193
+ const booksBy = query(Library).rule((r) => {
194
+ const { id, author, pages } = v(Book)
195
+ const { name } = v(Author)
695
196
  return r
696
- .match(Order, { id, state: "Shipped" })
697
- .match(Shipment, { order: id, carrier })
698
- .find({ id, carrier })
197
+ .match(Book, { id, author, pages })
198
+ .match(Author, { id: author, name })
199
+ .where(r.eq(name, r.param("name")))
200
+ .find({ id, pages })
699
201
  })
700
- ```
701
-
702
- ## Time and coverage
703
-
704
- ## 14. The calendar core
705
202
 
706
- Guarantee: Lean theorem + validator/runtime premises accepted equality is
707
- key-backed correspondence (`lean/Bumbledb/Dependencies.lean:
708
- keyed_eq_unique_correspondence`), while pointwise keys/coverage enforce only
709
- declared hard policy.
710
-
711
- Policy as schema: hard rules are pointwise keys, soft rules are the statements
712
- you decline to write. `Claim.source` carries the accepted attendance's id and
713
- declares nothing about it — the selected `mirrors` law below is what TYPES
714
- it: pairing `source` with `Attendance.id` puts it in the `"Attendance.id"`
715
- generator class. The class flows from the statement; no link declaration
716
- exists or is needed.
717
-
718
- ```ts
719
- const Rsvp = closed("Rsvp", ["Accepted", "Tentative", "Declined"])
720
- const Arm = closed("Arm", ["Busy", "Ooo"])
721
-
722
- const Person = relation("Person", { id: u64.fresh, name: str })
723
- const Room = relation("Room", { id: u64.fresh, name: str })
724
- const Event = relation("Event", { id: u64.fresh, span: interval(i64) })
725
- const Attendance = relation("Attendance", {
726
- id: u64.fresh,
727
- event: u64,
728
- person: u64,
729
- rsvp: Rsvp.id
730
- })
731
- const Claim = relation("Claim", {
732
- source: u64,
733
- person: u64,
734
- arm: Arm.id,
735
- span: interval(i64)
736
- })
737
- const Booking = relation("Booking", { room: u64, event: u64, span: interval(i64) })
738
- const WorkHours = relation("WorkHours", { person: u64, hours: interval(i64) })
739
-
740
- const Calendar = schema("Calendar", { Rsvp, Arm, Person, Room, Event, Attendance, Claim, Booking, WorkHours }, [
741
- contained(on(Attendance, "event"), on(Event, "id")),
742
- contained(on(Attendance, "person"), on(Person, "id")),
743
- contained(on(Attendance, "rsvp"), on(Rsvp, "id")),
744
- key(Attendance, ["event", "person"]), // one RSVP per (event, person)
745
- key(Claim, ["source"]),
746
- contained(on(Claim, "person"), on(Person, "id")),
747
- contained(on(Claim, "arm"), on(Arm, "id")),
748
- // HARD: rooms cannot double-book — the pointwise key (recipe 1's theorem).
749
- key(Booking, ["room", "span"]),
750
- // SOFT: people CAN double-book — key(Claim, ["person", "span"]) is simply
751
- // not declared. Policy is the presence or absence of one statement.
752
- // Accepting an invitation IS claiming the time (totality + validity) —
753
- // and this is the statement that types Claim.source:
754
- mirrors(on(Attendance.where({ rsvp: "Accepted" }), "id"), on(Claim.where({ arm: "Busy" }), "source")),
755
- // Busy time lies inside working hours, pointwise — coverage rides the
756
- // target's own key (disjoint + ordered is a theorem, not a request):
757
- key(WorkHours, ["person", "hours"]),
758
- contained(on(Claim.where({ arm: "Busy" }), ["person", "span"]), on(WorkHours, ["person", "hours"])),
759
- contained(on(Booking, "room"), on(Room, "id")),
760
- contained(on(Booking, "event"), on(Event, "id"))
761
- ])
762
-
763
- const roomConflicts = query(Calendar).rule((r) => {
764
- const { room, span } = v(Booking)
765
- return r
766
- .match(Booking, { room, span })
767
- .where(r.allen(span, ALLEN.intersects, r.param("want")))
768
- .find({ room, span })
769
- })
770
- const personLoad = query(Calendar).rule((r) => {
771
- const { person, span } = v(Claim)
772
- return r
773
- .match(Claim, { person, span })
774
- .where(r.allen(span, ALLEN.intersects, r.param("window")))
775
- .find({ person, span })
776
- })
203
+ const readBooks = (reader: QueryReader<typeof Library>, name: string) =>
204
+ Effect.scoped(
205
+ Effect.gen(function* () {
206
+ const result = yield* reader.execute(booksBy, { name }, work)
207
+ return yield* result.collect({ maxBytes: work.resultBytes }, work)
208
+ })
209
+ )
210
+ void readBooks
777
211
  ```
778
212
 
779
- ## 15. Effective-dated configuration
780
-
781
- Guarantee: Lean theorem/countermodel + validator/runtime premise — pointwise
782
- keys plus one-way support inclusion form a disjoint cover
783
- (`lean/Bumbledb/Dependencies.lean: pointwise_key_disjoint`,
784
- `coverage_is_support_inclusion`); target overhang is legal
785
- (`lean/Bumbledb/Countermodels.lean: one_way_overhang`).
213
+ ## 6. Grouped exact aggregates
786
214
 
787
- Versioned rules: no overlaps (pointwise key), no gaps in the policy's source
788
- lifetime (one-way coverage; version overhang remains legal), and "in force on
789
- date t" is one membership probe.
215
+ Aggregates fold the group's distinct full bindings: keep the identity-bearing
216
+ `id` in the binding set so equal scores in different rows both contribute.
217
+ Exact float `sum`/`mean` are deterministic with one final rounding.
790
218
 
791
219
  ```ts
792
- const Policy = relation("Policy", { id: u64.fresh, live: interval(i64) })
793
- const Version = relation("Version", { policy: u64, rate_bps: i64, valid: interval(i64) })
220
+ const Sample = relation("Sample", { id: uuid, series: uuid, value: f64 })
221
+ const Series = schema("Series", { Sample }, [key(Sample, ["id"])])
794
222
 
795
- const Pricing = schema("Pricing", { Policy, Version }, [
796
- contained(on(Version, "policy"), on(Policy, "id")),
797
- // No overlapping versions: at any instant, at most one rate is the law.
798
- key(Version, ["policy", "valid"]),
799
- // No gaps in the policy lifetime: every source point is covered by
800
- // versions. Together with the key above this is a disjoint cover, not an
801
- // exact partition: Version intervals may overhang (recipe 16).
802
- contained(on(Policy, ["id", "live"]), on(Version, ["policy", "valid"]))
803
- ])
804
-
805
- // in force on date t — one membership probe:
806
- const inForce = query(Pricing).rule((r) => {
807
- const { rate_bps, valid } = v(Version)
808
- return r
809
- .match(Version, { policy: r.param("p"), rate_bps, valid })
810
- .where(r.pointIn(r.param("t"), valid))
811
- .find({ rate_bps })
812
- })
813
- // clean successions (half-open makes MEETS exact, no ±1 fudge):
814
- const successions = query(Pricing).rule((r) => {
815
- const { policy: p, valid: a } = v(Version)
816
- const { valid: b } = v(Version)
817
- return r
818
- .match(Version, { policy: p, valid: a })
819
- .match(Version, { policy: p, valid: b })
820
- .where(r.allen(a, ALLEN.meets, b))
821
- .find({ a, b })
223
+ const stats = query(Series).rule((r) => {
224
+ const { id, series, value } = v(Sample)
225
+ return r.match(Sample, { id, series, value }).find({
226
+ series,
227
+ total: r.sum(value),
228
+ mean: r.mean(value),
229
+ points: r.count()
230
+ })
822
231
  })
232
+ void stats
823
233
  ```
824
234
 
825
- ## 16. Disjoint covers
826
-
827
- Guarantee: Lean theorem/countermodel + validator/runtime premise —
828
- `lean/Bumbledb/Dependencies.lean: coverage_is_support_inclusion` proves source
829
- coverage, not exact partition (`lean/Bumbledb/Countermodels.lean:
830
- one_way_overhang`).
235
+ ## 7. Nonrecursive composition — a template is a relation expression
831
236
 
832
- Pay periods, shifts, estimated-tax quarters: a pointwise key plus one-way
833
- coverage is a **disjoint cover** no overlaps among pay periods and no holes
834
- in the fiscal year's source span. Pay periods may extend beyond that span.
237
+ A typed query template of the same schema splices as a derived stage:
238
+ `v(imported)` mints variables for its head columns and `match(imported, …)`
239
+ joins them. Naming materializes nothing.
835
240
 
836
241
  ```ts
837
- const FiscalYear = relation("FiscalYear", { id: u64.fresh, span: interval(i64) })
838
- const PayPeriod = relation("PayPeriod", { year: u64, seq: u64, span: interval(i64) })
839
-
840
- const Payroll = schema("Payroll", { FiscalYear, PayPeriod }, [
841
- contained(on(PayPeriod, "year"), on(FiscalYear, "id")),
842
- key(PayPeriod, ["year", "seq"]), // sequence numbers stay unique
843
- key(PayPeriod, ["year", "span"]), // disjoint: no shared instant
844
- // Covering: no holes in the fiscal year's span; overhang is legal.
845
- contained(on(FiscalYear, ["id", "span"]), on(PayPeriod, ["year", "span"]))
242
+ const Reading = relation("Reading", { id: uuid, sensor: uuid, value: f64 })
243
+ const Sensor = relation("Sensor", { id: uuid, label: str })
244
+ const Telemetry = schema("Telemetry", { Reading, Sensor }, [
245
+ key(Reading, ["id"]),
246
+ key(Sensor, ["id"]),
247
+ contained(on(Reading, "sensor"), on(Sensor, "id"))
846
248
  ])
847
249
 
848
- // the period holding date t:
849
- const holding = query(Payroll).rule((r) => {
850
- const { seq, span } = v(PayPeriod)
851
- return r
852
- .match(PayPeriod, { year: r.param("y"), seq, span })
853
- .where(r.pointIn(r.param("t"), span))
854
- .find({ seq })
250
+ const perSensor = query(Telemetry).rule((r) => {
251
+ const { id, sensor, value } = v(Reading)
252
+ return r.match(Reading, { id, sensor, value }).find({ sensor, mean: r.mean(value) })
855
253
  })
856
- ```
857
-
858
- ## 17. Federal income tax
859
-
860
- Guarantee: validator/runtime premises + host discipline — keys prove bracket
861
- disjointness and statements prove residency coverage; full bracket coverage and proration are host duties.
862
-
863
- Brackets are intervals over money; the top bracket is a ray; regimes key on
864
- (year, status); and proration happens at write time, never at query time.
865
-
866
- ```ts
867
- const Status = closed("Status", ["Single", "MarriedJoint", "HeadOfHousehold"])
868
- const Regime = relation("Regime", { id: u64.fresh, year: i64, status: Status.id })
869
- const Bracket = relation("Bracket", { regime: u64, income: interval(i64), rate_bps: i64 })
870
- const Residency = relation("Residency", { person: u64, span: interval(i64) })
871
- // Split at write: an Earned fact never spans a year boundary — writers
872
- // split (prorate) at the boundary, so no reader ever clips. The
873
- // representation move that deletes clip-at-query (gravestone, recipe 23).
874
- const Earned = relation("Earned", { person: u64, regime: u64, span: interval(i64), minor: i64 })
875
254
 
876
- const Tax = schema("Tax", { Status, Regime, Bracket, Residency, Earned }, [
877
- contained(on(Regime, "status"), on(Status, "id")),
878
- key(Regime, ["year", "status"]), // one regime per (year, filing status)
879
- contained(on(Bracket, "regime"), on(Regime, "id")),
880
- // Brackets are disjoint per regime. Seed data conventionally covers
881
- // [0, ∞) and the top bracket is a ray, but this key proves disjointness
882
- // only. end == MAX denotes [s, ∞), an honest value of the representation,
883
- // not a sentinel (the point-domain law).
884
- key(Bracket, ["regime", "income"]),
885
- contained(on(Earned, "regime"), on(Regime, "id")),
886
- key(Residency, ["person", "span"]),
887
- // Residency exclusion: income counts only where earned inside a residency
888
- // period — pointwise coverage, the same judgment as recipe 15's. This
889
- // pair statement is also what puts the two bare `person` columns in one
890
- // (generator-less) class: "Residency.person", by least coordinate.
891
- contained(on(Earned, ["person", "span"]), on(Residency, ["person", "span"]))
892
- ])
893
-
894
- // the marginal bracket (membership probes the disjoint bracket set). Tax
895
- // owed is host arithmetic over the bracket walk — arithmetic beyond the
896
- // measure is refused (the ledger).
897
- const marginal = query(Tax).rule((r) => {
898
- const { id: reg } = v(Regime)
899
- const { income: b, rate_bps } = v(Bracket)
255
+ const labeled = query(Telemetry).rule((r) => {
256
+ const { sensor, mean } = v(perSensor)
257
+ const { label } = v(Sensor)
900
258
  return r
901
- .match(Regime, { id: reg, year: r.param("y"), status: r.param("s") })
902
- .match(Bracket, { regime: reg, income: b, rate_bps })
903
- .where(r.pointIn(r.param("taxable"), b))
904
- .find({ rate_bps })
905
- })
906
- ```
907
-
908
- ## 18. Free time and coalescing
909
-
910
- Guarantee: Lean theorem + runtime query semantics — `pack` coalesces answer
911
- intervals (`lean/Bumbledb/Query/Aggregates.lean: pack_canonical`,
912
- `pack_extensional`); it asserts no stored disjointness, completeness, or
913
- maintenance behavior.
914
-
915
- `pack` is Snodgrass's coalesce as an aggregate — maximal disjoint segments per
916
- group, one answer per (group, segment). Coalescing is never a write rule: the
917
- engine stores the claims it was given.
918
-
919
- ```ts
920
- const Person = relation("Person", { id: u64.fresh, name: str })
921
- const Claim = relation("Claim", { person: u64, span: interval(i64) })
922
-
923
- const FreeTime = schema("FreeTime", { Person, Claim }, [
924
- contained(on(Claim, "person"), on(Person, "id"))
925
- // No pointwise key, on purpose: claims overlap freely and pack coalesces
926
- // at read time. Wanting them stored-disjoint is recipe 1's key instead.
927
- ])
928
-
929
- // busy time, coalesced (adjacent segments merge — the half-open law):
930
- const busy = query(FreeTime).rule((r) => {
931
- const { person, span } = v(Claim)
932
- return r.match(Claim, { person, span }).find({ person, packed: r.pack(span) })
259
+ .match(perSensor, { sensor, mean })
260
+ .match(Sensor, { id: sensor, label })
261
+ .find({ label, mean })
933
262
  })
934
- // raw claimed time is host arithmetic on the interval endpoints every
935
- // answer row already carries (`end − start`). Overlaps double-count,
936
- // often the wrong question.
937
- // Coalesced totals = the two-query composition (pack, then a host fold) —
938
- // aggregates never nest; free time (gaps) is the two-line host walk over
939
- // sorted packed answers (host sorts by person then span start/end) — both
940
- // refusals recorded in the ledger.
263
+ void labeled
941
264
  ```
942
265
 
943
- ## The write side
944
-
945
- ## 19. The ledger
946
-
947
- Guarantee: Lean theorem + runtime invariant for bounded sums
948
- (`lean/Bumbledb/Query/Aggregates.lean: checkedSum_sound`); host discipline
949
- for double entry — statements resolve posting references, not arithmetic agreement.
266
+ ## 8. Bounded results: capped collect, one-shot page stream
950
267
 
951
- The census workload. Balance is a query, never a column.
268
+ `collect({ maxBytes }, work)` is database-enforced total materialization and
269
+ leaves the result available. `pages({ pageBytes }, work)` is a ONE-SHOT consuming
270
+ stream over the completed result: the first run moves the backing into a
271
+ private scoped cursor; a second run refuses. Every element is one owned page
272
+ array — pages, not rows — delivered after complete evaluation.
952
273
 
953
274
  ```ts
954
- const Account = relation("Account", { id: u64.fresh, name: str })
955
- const JournalEntry = relation("JournalEntry", { id: u64.fresh, at: i64, memo: str })
956
- const Posting = relation("Posting", {
957
- id: u64.fresh,
958
- entry: u64,
959
- account: u64,
960
- minor: i64
961
- })
962
-
963
- const Ledger = schema("Ledger", { Account, JournalEntry, Posting }, [
964
- contained(on(Posting, "entry"), on(JournalEntry, "id")),
965
- contained(on(Posting, "account"), on(Account, "id"))
966
- // A stored balance column equaling Sum(postings) is the arithmetic-
967
- // agreement statement — refused (the ledger): statements prove presence
968
- // and topology, never that a value equals a computation. Balance is host
969
- // arithmetic over sum; a materialized rollup is recipe 21's shape.
970
- ])
275
+ const Event = relation("Event", { id: uuid, at: i64 })
276
+ const Feed = schema("Feed", { Event }, [key(Event, ["id"])])
971
277
 
972
- // balances (bind the fresh id — set semantics collapses duplicates):
973
- const balances = query(Ledger).rule((r) => {
974
- const { id, account, minor } = v(Posting)
975
- return r.match(Posting, { id, account, minor }).find({ account, balance: r.sum(minor) })
278
+ const everything = query(Feed).rule((r) => {
279
+ const { id, at } = v(Event)
280
+ return r.match(Event, { id, at }).find({ id, at })
976
281
  })
977
- // double-entry audit (host asserts every total is 0 — discipline, not schema):
978
- const doubleEntry = query(Ledger).rule((r) => {
979
- const { id, entry, minor } = v(Posting)
980
- return r.match(Posting, { id, entry, minor }).find({ entry, balance: r.sum(minor) })
981
- })
982
- ```
983
-
984
- ## 20. Conditional writes
985
-
986
- Guarantee: Lean theorem + generation-witness/runtime premise + host retry
987
- discipline — snapshot-derived writes detect movement
988
- (`lean/Bumbledb/Txn.lean: writeFrom_moved`, `witness_conflict_distinct`);
989
- final-state point reads need no earlier witness.
990
-
991
- The generation witness: read the model, propose a delta, commit iff the model
992
- you read is still the model. The SDK ships one-shot `writeFrom` (the witness
993
- escapes the read callback; the instance does not). Retry on
994
- `{ tag: "moved" }` is host policy — a short loop around `db.read` +
995
- `writeFrom` if the host wants it. `abandon(payload)`
996
- declines to commit without issuing anything — from `db.write` and
997
- `db.writeFrom` alike (the sentinel's contract is unconditional, and
998
- the outcome arm is in the result type exactly when the callback can
999
- abandon).
1000
282
 
1001
- ```ts
1002
- const State = closed("State", ["Queued", "Running", "Done"])
1003
- const Job = relation("Job", { id: u64.fresh, state: State.id, payload: str })
1004
- const Lease = relation("Lease", { job: u64, worker: u64, until: i64 })
1005
-
1006
- const Jobs = schema("Jobs", { State, Job, Lease }, [
1007
- contained(on(Job, "state"), on(State, "id")),
1008
- key(Lease, ["job"]),
1009
- // A lease exists iff its job is Running (recipe 13's conditional target):
1010
- // claiming a job and leasing it commit together or not at all.
1011
- mirrors(on(Lease, "job"), on(Job.where({ state: "Running" }), "id"))
1012
- ])
1013
-
1014
- // update-where's premise — "still Queued" is the witness:
1015
- const stillQueued = query(Jobs).rule((r) => {
1016
- const { id, payload } = v(Job)
1017
- return r.match(Job, { id, state: "Queued", payload }).find({ id, payload })
1018
- })
1019
-
1020
- const created = await Db.create("./jobs.db", Jobs)
1021
- if (created.tag !== "accepted") {
1022
- throw new Error("create rejected")
1023
- }
1024
- const db = created.value
1025
- const prepared = db.prepare(stillQueued)
1026
-
1027
- // The witnessed write: premise reads via `instance`, the delta via `tx`.
1028
- // On a moved generation `writeFrom` returns `{ tag: "moved" }` — retry is
1029
- // host policy. The other two idioms: insert-select is the same shape (query
1030
- // source answers, insert the derived facts); key-shaped read-modify-write
1031
- // uses `tx.get`/`tx.contains` — final-state point reads need no earlier
1032
- // witness.
1033
- const outcome = db.read(function attempt(instance, witness) {
1034
- return db.writeFrom(witness, function updateWhere(tx) {
1035
- const queued = instance.execute(prepared, {})
1036
- if (queued.length === 0) {
1037
- return abandon("nothing queued")
1038
- }
1039
- for (const row of queued) {
1040
- tx.delete(Job, [{ id: row.id, state: "Queued", payload: row.payload }])
1041
- tx.insert(Job, [{ id: row.id, state: "Running", payload: row.payload }])
1042
- tx.insert(Lease, [{ job: row.id, worker: 7n, until: 60n }])
1043
- }
1044
- return undefined
1045
- })
1046
- })
283
+ const drain = (result: CompleteResult<{ readonly id: Uuid; readonly at: bigint }>) =>
284
+ result.pages({ pageBytes: 65536n }, work).pipe(
285
+ Stream.runForEach((page) =>
286
+ Effect.sync(() => {
287
+ // One owned page array; caller mutation cannot reach native
288
+ // state or another delivered page.
289
+ return page.length
290
+ })
291
+ )
292
+ )
293
+ void [everything, drain]
1047
294
  ```
1048
295
 
1049
- ## 21. Derived relations
1050
-
1051
- Guarantee: Lean theorem + validator/runtime premises for soundness
1052
- (`lean/Bumbledb/Txn.lean: derived_soundness_vs_freshness`); host
1053
- discipline for completeness — containment rejects unsupported facts but never refreshes omissions.
296
+ ## 9. Application identity: generate once, retain, never regenerate
1054
297
 
1055
- The materialized view as a relation under statements unsoundness the schema
1056
- can name is uncommittable; incompleteness remains representable until the host
1057
- refreshes it.
298
+ `Effect.sync(() => crypto.randomUUID())` is effectful cryptographic entropyrun it once for an
299
+ original intent, persist the value with the request, and never regenerate
300
+ inside a retry. `Uuid.parse` is the pure fixed-size parser returning
301
+ `Result`.
1058
302
 
1059
303
  ```ts
1060
- const Arm = closed("Arm", ["Busy", "Ooo"])
1061
- const Claim = relation("Claim", { source: u64, person: u64, arm: Arm.id, span: interval(i64) })
1062
- const BusySpan = relation("BusySpan", { person: u64, span: interval(i64) })
304
+ const parsed = Uuid.parse("00112233-4455-6677-8899-aabbccddeeff")
305
+ const okOrRefused: boolean = Result.isSuccess(parsed)
1063
306
 
1064
- const Rollup = schema("Rollup", { Arm, Claim, BusySpan }, [
1065
- contained(on(Claim, "arm"), on(Arm, "id")),
1066
- key(Claim, ["source"]),
1067
- key(Claim, ["person", "span"]),
1068
- key(BusySpan, ["person", "span"]), // packed ⇒ disjoint: statable
1069
- // Soundness, pointwise: every stored rollup point is covered by busy
1070
- // claims — an UNSOUND rollup (claiming busy time that isn't, or surviving
1071
- // its sources' deletion) cannot commit, judged on every touching commit.
1072
- contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
1073
- ])
1074
-
1075
- // Maintenance is the third witness idiom (recipe 20): re-run the deriving
1076
- // query on a snapshot, diff, commit witnessed — the rollup cannot commit
1077
- // against sources it didn't actually read. The deriving query (pack IS the
1078
- // coalesce):
1079
- const deriving = query(Rollup).rule((r) => {
1080
- const { person, span } = v(Claim)
1081
- return r.match(Claim, { person, span, arm: "Busy" }).find({ person, packed: r.pack(span) })
307
+ const mintOnce = Effect.gen(function* () {
308
+ const id = yield* Effect.sync(() => crypto.randomUUID())
309
+ // Persist `id` with the original request BEFORE any database dispatch;
310
+ // a timeout retries the identical intent, never a new identity.
311
+ return id
1082
312
  })
313
+ void [okOrRefused, mintOnce]
1083
314
  ```
1084
315
 
1085
- ## 22. Union reads
1086
-
1087
- Guarantee: Lean theorem + represented planner/runtime premise — rule union is
1088
- set-idempotent (`lean/Bumbledb/Query/Denotation.lean: union_idempotent`);
1089
- key-backed DU arms satisfy the disjoint-arms licence
1090
- (`lean/Bumbledb/Exec/Dedup.lean: disjoint_witness_licence`); execution
1091
- always keeps one spanning seen-set.
316
+ ## 10. Witnessed correction: exact expected state
1092
317
 
1093
- The whole-DU read is a set of rules: one head, one rule per arm — disjunction
1094
- is data at the top, never an execution node.
318
+ Read under a short scope, keep the copied witness, and apply with
319
+ `expected: { kind: "exact", at: witness }`. An intervening net change moves
320
+ the apply instead of silently overwriting.
1095
321
 
1096
322
  ```ts
1097
- const Kind = closed("Kind", ["Card", "Ach"])
1098
- const Payment = relation("Payment", { id: u64.fresh, kind: Kind.id })
1099
- const Card = relation("Card", { payment: u64, last4: u64 })
1100
- const Ach = relation("Ach", { payment: u64, routing: u64 })
323
+ const Account = relation("Account", { id: uuid, balance: i64 })
324
+ const Bank = schema("Bank", { Account }, [key(Account, ["id"])])
1101
325
 
1102
- const Payments = schema("Payments", { Kind, Payment, Card, Ach }, [
1103
- contained(on(Payment, "kind"), on(Kind, "id")),
1104
- key(Card, ["payment"]),
1105
- key(Ach, ["payment"]),
1106
- mirrors(on(Payment.where({ kind: "Card" }), "id"), on(Card, "payment")),
1107
- mirrors(on(Payment.where({ kind: "Ach" }), "id"), on(Ach, "payment"))
1108
- ])
1109
-
1110
- // One query, two rules (set union). The exclusivity theorem (recipe 2) is
1111
- // spent a third time here: rules selecting different `kind` handles are
1112
- // provably disjoint. Execution still probes one spanning seen-set — the
1113
- // measured refutation deleted the elision.
1114
- const wholeDu = query(Payments)
1115
- .rule((r) => {
1116
- const { id } = v(Payment)
1117
- const { last4: n } = v(Card)
1118
- return r
1119
- .match(Payment, { id, kind: "Card" })
1120
- .match(Card, { payment: id, last4: n })
1121
- .find({ id, n })
1122
- })
1123
- .rule((r) => {
1124
- const { id } = v(Payment)
1125
- const { routing: n } = v(Ach)
1126
- return r
1127
- .match(Payment, { id, kind: "Ach" })
1128
- .match(Ach, { payment: id, routing: n })
1129
- .find({ id, n })
1130
- })
1131
- ```
1132
-
1133
- ## 23. The anti-recipes: five gravestones
1134
-
1135
- Guarantee: intentionally refused — each gravestone names unsupported vocabulary
1136
- and its representable replacement; none asserts an engine theorem.
1137
-
1138
- What not to model. Each gravestone cites its replacement; the block's
1139
- relations are the replacements, compiled.
1140
-
1141
- ```ts
1142
- // GRAVESTONE: successor pointers (a `next` column). A linked list inside a
1143
- // relation is control flow smuggled into data. REPLACEMENT: the ordering
1144
- // triple (recipe 9).
1145
- const Step = relation("Step", { flow: u64, pos: u64, action: str })
1146
- // GRAVESTONE: floats for scores, rates, money. Permanently refused (the
1147
- // ledger). REPLACEMENT: fixed-point i64 — basis points (recipe 4).
1148
- const Score = relation("Score", { subject: u64, bps: i64 })
1149
- // GRAVESTONE: conditional keys ("at most one active run per student") —
1150
- // rejected as FDs. REPLACEMENT: the relation split, whose ordinary key IS
1151
- // the invariant (recipe 13's arm shape).
1152
- const ActiveRun = relation("ActiveRun", { student: u64, run: u64 })
1153
- // GRAVESTONE: clip-at-query intervals (facts spanning period boundaries,
1154
- // every reader clipping). REPLACEMENT: split at write (recipe 17).
1155
- const Usage = relation("Usage", { meter: u64, period: u64, used: interval(i64) })
1156
- // GRAVESTONE: uuid keys. uuidv7 is identity + clash-avoidance + clock in
1157
- // one lie. REPLACEMENT: fresh (minted identity) + an explicit i64 time column.
1158
- const Event = relation("Event", { id: u64.fresh, at: i64 })
1159
-
1160
- const Gravestones = schema("Gravestones", { Step, Score, ActiveRun, Usage, Event }, [
1161
- key(Step, ["flow", "pos"]),
1162
- key(Score, ["subject"]),
1163
- key(ActiveRun, ["student"]),
1164
- key(Usage, ["meter", "used"])
1165
- ])
1166
- ```
1167
-
1168
- ## Host-driven closure
1169
-
1170
- ## 24. The closure idiom
1171
-
1172
- Guarantee: host discipline for the loop — the finite `seen` set proves
1173
- termination for the host run; the engine-native form beside it executes
1174
- whole under the linear reach driver, budget-bounded
1175
- (`lean/Bumbledb/Exec/Reach.lean: evalLinearReach_eq_lfp` /
1176
- `evalQuery_sound`).
1177
-
1178
- Reachability, in two dialects. The host-loop idiom remains the depth-bounded
1179
- answer: the loop runs depth-many rounds and each round is one ∈-set query —
1180
- an `inSet` probe, microsecond-class. The frontier discipline below *is*
1181
- semi-naive evaluation's Δ, spent where a loop is a loop: the host. The
1182
- engine-native form is the same closure as one query with `.reach()`.
1183
-
1184
- ```ts
1185
- const Node = relation("Node", { id: u64.fresh, name: str })
1186
- // One parent per child — a forest (recipe 10's edge shape); a root is a
1187
- // node whose Parent fact is absent (recipe 3's honest 0..1).
1188
- const Parent = relation("Parent", { child: u64, parent: u64 })
1189
-
1190
- const Closure = schema("Closure", { Node, Parent }, [
1191
- key(Parent, ["child"]),
1192
- contained(on(Parent, "child"), on(Node, "id")),
1193
- contained(on(Parent, "parent"), on(Node, "id"))
1194
- ])
1195
-
1196
- // The loop's one query — the frontier's children, one ∈-set probe:
1197
- const step = query(Closure).rule((r) => {
1198
- const { child: c } = v(Parent)
1199
- return r.match(Parent, { child: c, parent: r.inSet("frontier") }).find({ c })
1200
- })
1201
- ```
1202
-
1203
- The loop (the compiled, driven copy is in `test/cookbook.test.ts`, over a
1204
- three-level forest with the exact reachable set asserted):
1205
-
1206
- ```ts
1207
- const created = await Db.create("./closure.db", Closure)
1208
- if (created.tag !== "accepted") {
1209
- throw new Error("create rejected")
1210
- }
1211
- const db = created.value
1212
- const stepPrepared = db.prepare(step)
1213
- const root = 1n // the host's chosen root node id
1214
-
1215
- const seen = new Set<bigint>([root])
1216
- let frontier: readonly bigint[] = [root]
1217
- for (;;) {
1218
- const next = db.read((i) => i.execute(stepPrepared, { frontier })) // one set-param query
1219
- const fresh = next
1220
- .map((row) => row.c)
1221
- .filter((c) => {
1222
- return !seen.has(c)
326
+ const correct = (accountId: Uuid) =>
327
+ Effect.scoped(
328
+ Effect.gen(function* () {
329
+ const db = yield* Db.open(localPath, Bank, work)
330
+ const observed = yield* Effect.scoped(
331
+ Effect.gen(function* () {
332
+ const snapshot = yield* db.snapshot(work)
333
+ const previous = yield* snapshot.get(Account, { id: accountId }, work)
334
+ if (Option.isNone(previous)) {
335
+ return yield* Effect.fail({ missing: accountId })
336
+ }
337
+ return { previous: previous.value, at: snapshot.witness }
338
+ })
339
+ )
340
+ const draft = yield* ChangeSet.builder(Bank, work)
341
+ yield* draft.delete(Account, [observed.previous])
342
+ yield* draft.insert(Account, [{ ...observed.previous, balance: observed.previous.balance + 1n }])
343
+ const changes = yield* draft.finish()
344
+ return yield* db.apply(changes, { ...work, expected: { kind: "exact", at: observed.at } })
1223
345
  })
1224
- if (fresh.length === 0) {
1225
- break
1226
- }
1227
- for (const c of fresh) {
1228
- seen.add(c)
1229
- }
1230
- frontier = fresh
1231
- }
1232
- ```
1233
-
1234
- Termination is the host's theorem: `seen` grows strictly or the loop breaks,
1235
- inside a finite node set. When the idiom's costs bite — **unbounded or large
1236
- depth**, or **closure composed into a larger plan** — write the engine-native
1237
- form instead: `?root` seeds the rec, and the main rule is the finished
1238
- set's own identity projection (an interior atom is a positive occurrence, so it
1239
- grounds its variables — no re-grounding join over a domain relation exists):
1240
-
1241
- ```ts
1242
- const reach = query(Closure)
1243
- .reach("reach", {
1244
- base: [
1245
- (r) => {
1246
- const { id: c } = v(Node)
1247
- return r
1248
- .match(Node, { id: c })
1249
- .where(r.eq(c, r.param("root")))
1250
- .find({ c })
1251
- }
1252
- ],
1253
- rec: [
1254
- (r) => {
1255
- const { child: c, parent } = v(Parent)
1256
- return r.match(Parent, { child: c, parent }).interior("reach", { c: parent }).find({ c })
1257
- }
1258
- ]
1259
- })
1260
- .rule((r) => {
1261
- const { id: c } = v(Node)
1262
- return r.interior("reach", { c }).find({ c })
1263
- })
1264
- const reachPrepared = db.prepare(reach)
1265
- ```
1266
-
1267
- The complement is one `r.not` away — negation **of** a finished rec is
1268
- engine-legal (negation in rec is refused), so
1269
- "every node the closure never reached" runs in-plan through the engine's
1270
- anti-probe, never as a host-side set difference — the same query with the
1271
- main rule:
1272
-
1273
- ```ts
1274
- const unreached = query(Closure)
1275
- .reach("reach", {
1276
- base: [
1277
- (r) => {
1278
- const { id: c } = v(Node)
1279
- return r
1280
- .match(Node, { id: c })
1281
- .where(r.eq(c, r.param("root")))
1282
- .find({ c })
1283
- }
1284
- ],
1285
- rec: [
1286
- (r) => {
1287
- const { child: c, parent } = v(Parent)
1288
- return r.match(Parent, { child: c, parent }).interior("reach", { c: parent }).find({ c })
1289
- }
1290
- ]
1291
- })
1292
- .rule((r) => {
1293
- const { id: c } = v(Node)
1294
- return r.match(Node, { id: c }).where(r.not("reach", { c })).find({ c })
1295
- })
1296
- ```
1297
-
1298
- (the test drives both dialects and asserts the same reachable sets, root for
1299
- root). What stays host-side is the **chain-window class** — interval
1300
- intersection along paths — which the recursion surface fences out: the idiom
1301
- carries the window in the host's frontier, one intersection per hop, and that
1302
- composition has no engine form.
1303
-
1304
- ## 25. The chart of accounts
1305
-
1306
- Guarantee: host discipline + runtime aggregate semantics — the host computes
1307
- closure, then one checked `sum` (`lean/Bumbledb/Query/Aggregates.lean:
1308
- checkedSum_sound`); the engine-native form folds over a finished rec,
1309
- the one aggregation shape the rec roster admits.
1310
-
1311
- The ledger workload's real recursion case, in the same two dialects: a
1312
- hierarchical chart of accounts and a subtree rollup. The host composition —
1313
- recipe 24's loop accumulates the subtree's ∈-set, then **one `sum` query over
1314
- the accumulated set** folds the postings. The engine aggregates, the host
1315
- composes (aggregates never nest). The engine-native form is one query:
1316
- aggregation *through* a cycle is refused, but a fold over a finished rec
1317
- from main reads a finished set and is ordinary.
1318
-
1319
- ```ts
1320
- const Account = relation("Account", { id: u64.fresh, name: str })
1321
- const AccountParent = relation("AccountParent", { child: u64, parent: u64 })
1322
- const Posting = relation("Posting", { id: u64.fresh, account: u64, minor: i64 })
1323
-
1324
- const Accounts = schema("Accounts", { Account, AccountParent, Posting }, [
1325
- key(AccountParent, ["child"]), // one parent per account
1326
- contained(on(AccountParent, "child"), on(Account, "id")),
1327
- contained(on(AccountParent, "parent"), on(Account, "id")),
1328
- contained(on(Posting, "account"), on(Account, "id"))
1329
- ])
1330
-
1331
- // The two queries the host rollup composes:
1332
- // the frontier step (recipe 24's loop, verbatim):
1333
- const frontierStep = query(Accounts).rule((r) => {
1334
- const { child: c } = v(AccountParent)
1335
- return r.match(AccountParent, { child: c, parent: r.inSet("frontier") }).find({ c })
1336
- })
1337
- // the rollup over the accumulated subtree (bind the fresh id — recipe
1338
- // 19's discipline, spent again; equal postings to one account both count):
1339
- const subtreeRollup = query(Accounts).rule((r) => {
1340
- const { id, minor } = v(Posting)
1341
- return r.match(Posting, { id, account: r.inSet("subtree"), minor }).find({ total: r.sum(minor) })
1342
- })
1343
- // The engine-native form: the rec converges first, then the
1344
- // main fold runs once over the finished subtree.
1345
- const nativeRollup = query(Accounts)
1346
- .reach("sub", {
1347
- base: [
1348
- (r) => {
1349
- const { id: a } = v(Account)
1350
- return r
1351
- .match(Account, { id: a })
1352
- .where(r.eq(a, r.param("root")))
1353
- .find({ a })
1354
- }
1355
- ],
1356
- rec: [
1357
- (r) => {
1358
- const { child: a, parent } = v(AccountParent)
1359
- return r.match(AccountParent, { child: a, parent }).interior("sub", { a: parent }).find({ a })
1360
- }
1361
- ]
1362
- })
1363
- .rule((r) => {
1364
- const { id, account: a, minor } = v(Posting)
1365
- return r
1366
- .match(Posting, { id, account: a, minor })
1367
- .interior("sub", { a })
1368
- .find({ total: r.sum(minor) })
1369
- })
1370
- ```
1371
-
1372
- ## 26. Exact partition
1373
-
1374
- Guarantee: Lean theorem + validator/runtime premises — mutual point coverage
1375
- plus pointwise keys realizes exact partition
1376
- (`lean/Bumbledb/Dependencies.lean: exact_partition_iff`).
1377
-
1378
- An exact partition needs both coverage directions. The first containment below
1379
- is the intent-level reference; the two pointwise keys make each side disjoint;
1380
- the final pair proves equal point supports per policy — forward coverage
1381
- forbids gaps and reverse coverage forbids overhang.
1382
-
1383
- The explicit `key(Policy, ["id", "live"])` is load-bearing. Containment
1384
- targets resolve by their exact projected field set, so the fresh `{id}` key
1385
- cannot serve the `{id, live}` target and the engine infers no key closure.
1386
-
1387
- ```ts
1388
- const Policy = relation("Policy", { id: u64.fresh, live: interval(i64) })
1389
- const Version = relation("Version", { policy: u64, valid: interval(i64) })
1390
-
1391
- const ExactPartition = schema("ExactPartition", { Policy, Version }, [
1392
- contained(on(Version, "policy"), on(Policy, "id")), // reference intent
1393
- key(Version, ["policy", "valid"]), // disjoint versions
1394
- key(Policy, ["id", "live"]), // exact target key, not implied by {id}
1395
- contained(on(Policy, ["id", "live"]), on(Version, ["policy", "valid"])), // no gaps in the policy source span
1396
- contained(on(Version, ["policy", "valid"]), on(Policy, ["id", "live"])) // no version overhang
1397
- ])
1398
- ```
1399
-
1400
- Together the mutual containments prove equal point supports for each policy;
1401
- the pointwise keys make those supports genuine partitions rather than
1402
- overlapping covers. Touching half-open segments remain legal, and the same
1403
- construction works with any scalar-prefix arity before the final interval
1404
- position.
1405
-
1406
- ## 27. Derived facts, maintained
1407
-
1408
- Guarantee: host discipline + validator/runtime premises — freshness comes from
1409
- the generation witness; containment proves surviving rollup facts sound only
1410
- (`lean/Bumbledb/Txn.lean: derived_soundness_vs_freshness`).
1411
-
1412
- A stored rollup is an ordinary relation with an ordinary soundness statement.
1413
- Here `pack` derives maximal busy spans, while containment prevents any stored
1414
- `BusySpan` point that has no busy claim behind it. That is soundness, not a
1415
- refresh theorem: a missing span remains representable until the host
1416
- maintenance loop fills it.
1417
-
1418
- ```ts
1419
- const Arm = closed("Arm", ["Busy", "Ooo"])
1420
- const Claim = relation("Claim", { source: u64, person: u64, arm: Arm.id, span: interval(i64) })
1421
- const BusySpan = relation("BusySpan", { person: u64, span: interval(i64) })
1422
-
1423
- const MaintainedRollup = schema("MaintainedRollup", { Arm, Claim, BusySpan }, [
1424
- contained(on(Claim, "arm"), on(Arm, "id")),
1425
- key(Claim, ["source"]),
1426
- key(Claim, ["person", "span"]),
1427
- key(BusySpan, ["person", "span"]),
1428
- contained(on(BusySpan, ["person", "span"]), on(Claim.where({ arm: "Busy" }), ["person", "span"]))
1429
- ])
1430
-
1431
- // Derive the desired rollup on the maintenance snapshot:
1432
- const deriving = query(MaintainedRollup).rule((r) => {
1433
- const { source, person, span } = v(Claim)
1434
- return r.match(Claim, { source, person, arm: "Busy", span }).find({ person, packed: r.pack(span) })
1435
- })
1436
- ```
1437
-
1438
- The host loop is `db.read` + `writeFrom`: derive on the attempt's snapshot, diff,
1439
- build the delta — recipe 20's third idiom. On a moved generation the host
1440
- throws away the attempt and retries on a fresh snapshot; it
1441
- never retries a stale diff. Dependencies prove every surviving stored span
1442
- sound, while the witness proves which source state the derivation saw;
1443
- neither mechanism proves completeness. The engine's compiled copy
1444
- (`maintain_busy_spans` in `cookbook.rs`) drives the retry-and-repack loop.
1445
-
1446
- ## Operating the store
1447
-
1448
- ## 28. Migration is ETL
1449
-
1450
- Guarantee: Lean theorem + validator/runtime premises + host discipline —
1451
- fingerprints refuse reinterpretation, final-state judgments validate each load
1452
- (`lean/Bumbledb/Txn.lean: etl_lands_valid`), and the host owns
1453
- the semantic transform and dependency-safe load order.
1454
-
1455
- There is no in-place migration and never will be: a schema is a theory, the
1456
- store records the theory's fingerprint, and `Db.open` under a changed theory
1457
- is a hard fingerprint mismatch — the engine refuses to reinterpret facts it
1458
- judged under different laws. Migration is extract, transform, load: `scan`
1459
- exports every fact of a relation as typed values under one snapshot, the host
1460
- transforms, and inserts (fresh ids resupplied — identity survives) land into
1461
- a store created under the new theory, judged whole by the ordinary
1462
- final-state judgment: load containment targets first, and a chunk that lands
1463
- is already valid.
1464
-
1465
- The v2 theory below adds what v1 never recorded — *when* a salary applied —
1466
- as an interval with a pointwise key: one salary per employee per instant. The
1467
- transform supplies the missing dimension (a ray from the migration epoch).
1468
-
1469
- ```ts
1470
- // The old theory, judged and fingerprinted:
1471
- const EmployeeV1 = relation("Employee", { id: u64.fresh, name: str })
1472
- const SalaryV1 = relation("Salary", { employee: u64, amount: i64 })
1473
- const PayrollV1 = schema("PayrollV1", { Employee: EmployeeV1, Salary: SalaryV1 }, [
1474
- contained(on(SalaryV1, "employee"), on(EmployeeV1, "id"))
1475
- ])
1476
-
1477
- // The new theory adds what v1 never recorded:
1478
- const Employee = relation("Employee", { id: u64.fresh, name: str })
1479
- const Salary = relation("Salary", { employee: u64, amount: i64, applies: interval(i64) })
1480
- const Payroll = schema("Payroll", { Employee, Salary }, [
1481
- contained(on(Salary, "employee"), on(Employee, "id")),
1482
- key(Salary, ["employee", "applies"]) // one salary per instant
1483
- ])
1484
-
1485
- // The post-migration read — salaries in force at an instant:
1486
- const inForceAt = query(Payroll).rule((r) => {
1487
- const { id: e, name } = v(Employee)
1488
- const { amount, applies: w } = v(Salary)
1489
- return r
1490
- .match(Employee, { id: e, name })
1491
- .match(Salary, { employee: e, amount, applies: w })
1492
- .where(r.pointIn(r.param("at"), w))
1493
- .find({ name, amount })
1494
- })
1495
- ```
1496
-
1497
- The engine's compiled test drives the whole loop (export under one snapshot,
1498
- the fingerprint refusal, load order, identity, mint catch-up, judgment); the
1499
- SDK pin asserts the two theories carry two distinct fingerprints — the
1500
- refusal's premise. For stores whose creating schema is gone, `Db.exhume`
1501
- reads the store's own persisted descriptor — the one schema-independent read
1502
- path, the E half of ETL. Its lifetime is a `using` scope (`using exhumed =
1503
- await Db.exhume(oldPath)`): disposal releases the store's exclusive lock at
1504
- scope exit, so the rebirth loop's same-path retry never waits on GC.
1505
-
1506
- ## Composition
1507
-
1508
- ## 29. The zone ledger
1509
-
1510
- Guarantee: Lean theorem + validator/runtime premises — per-kind mutual point
1511
- coverage realizes each arm's exact partition
1512
- (`lean/Bumbledb/Dependencies.lean: exact_partition_iff`) over one
1513
- disjointness witness, and the mixed-width `==` positions type by element
1514
- domain (`lean/Bumbledb/Schema.lean: Value.points_one_tag_u64`); witness
1515
- segmentation is host discipline (the honesty note below).
1516
-
1517
- Recipe 9's sidecar, composed: a ledger whose timeline divides into zones of
1518
- two kinds — unit zones (`interval(u64, 1n)`) and pair zones
1519
- (`interval(u64, 2n)`), each kind carrying its own payload sidecar. The
1520
- discriminated-union pattern (recipe 2) applied at interval positions: a
1521
- kind-discriminated `Zone` witness relation owns **cross-sidecar disjointness**
1522
- through its one pointwise key, and since each sidecar's point support equals
1523
- its kind's zone support (the per-kind `mirrors`), a unit slot can never
1524
- overlap a pair slot even though they live in different relations. The arm
1525
- widths are enforced **by type**: a `UnitSlot` value is width 1 or does not
1526
- exist — no runtime width check, nothing to enforce at commit.
1527
-
1528
- ```ts
1529
- const Kind = closed("Kind", ["Unit", "Pair"])
1530
- const Ledger = relation("Ledger", { id: u64.fresh, name: str })
1531
- // The witness: every zone of the ledger, kind-discriminated; its one
1532
- // pointwise key is the cross-sidecar disjointness proof.
1533
- const Zone = relation("Zone", { ledger: u64, kind: Kind.id, at: interval(u64) })
1534
- const UnitSlot = relation("UnitSlot", { ledger: u64, at: interval(u64, 1n), entry: u64 })
1535
- const PairSlot = relation("PairSlot", { ledger: u64, at: interval(u64, 2n), entry: u64 })
1536
-
1537
- const ZoneLedger = schema("ZoneLedger", { Kind, Ledger, Zone, UnitSlot, PairSlot }, [
1538
- contained(on(Zone, "ledger"), on(Ledger, "id")),
1539
- contained(on(Zone, "kind"), on(Kind, "id")),
1540
- key(Zone, ["ledger", "at"]), // all zones disjoint, whatever the kind
1541
- key(UnitSlot, ["ledger", "at"]),
1542
- key(PairSlot, ["ledger", "at"]),
1543
- // Each kind's zones carry exactly its sidecar's points — mixed widths,
1544
- // one element domain:
1545
- mirrors(on(Zone.where({ kind: "Unit" }), ["ledger", "at"]), on(UnitSlot, ["ledger", "at"])),
1546
- mirrors(on(Zone.where({ kind: "Pair" }), ["ledger", "at"]), on(PairSlot, ["ledger", "at"]))
1547
- ])
346
+ )
347
+ void correct
1548
348
  ```
1549
349
 
1550
- The honesty note **coalescing insensitivity**: the `mirrors` judgments
1551
- compare point supports, not rows. A single Unit-kind zone `[4,6)` beside two
1552
- unit slots `[4,5)`, `[5,6)` satisfies both directions, because nothing forces
1553
- the witness rows to mirror the sidecar's segmentation — only its points. If
1554
- per-row correspondence matters, the host writes zones at slot granularity;
1555
- the schema proves disjointness and coverage either way.
1556
-
1557
- ## Point reads
350
+ ## 11. Exact floats and dense intervals
1558
351
 
1559
- ## 30. The keyed read
1560
-
1561
- Guarantee: validator/runtime premisesa declared key FD admits at most one
1562
- fact per determinant tuple (the key phase of the commit judgment), and every
1563
- keyed point read answers exactly that fact or nothing, on every scope
1564
- (`ts/test/keyed-get.test.ts`; the engine half is
1565
- `crates/bumbledb/tests/keyed_get.rs`).
1566
-
1567
- The key is a **law**, and the read surface is that law made callable. The
1568
- schema says `key(Course, ["grp"])` — one course per group — so "the
1569
- course of a group" is a well-posed question with at most one answer, and
1570
- the store already enforces that on every commit. Hold the statement VALUE:
1571
- it is the read's selector below (statement identity is the membership rule).
1572
-
1573
- ```ts
1574
- const Grp = relation("Grp", { id: u64.fresh, label: str })
1575
- const Course = relation("Course", { id: u64.fresh, grp: u64, title: str })
1576
- // The law: one course per group — the callable key.
1577
- const courseGrpKey = key(Course, ["grp"])
1578
-
1579
- const KeyedRead = schema("KeyedRead", { Grp, Course }, [
1580
- contained(on(Course, "grp"), on(Grp, "id")),
1581
- courseGrpKey
1582
- ])
1583
- ```
1584
-
1585
- The point read is the statement handed back to `get` — one spelling on
1586
- every scope (the symmetry rule): `db.get` standalone, `snap.get` inside a
1587
- read scope, `tx.get` inside a write transaction, where the transaction side
1588
- answers the FINAL state (base plus pending delta: read-your-writes, a
1589
- pending delete answers `undefined`). The key object is typed by the
1590
- statement's own projection — a wrong field name is a compile error, never a
1591
- runtime shape check. The primary 2-arg form needs no statement: the fresh
1592
- field IS the primary key.
352
+ `f64` is a real schema scalar: NaN canonicalizes to the one quiet NaN,
353
+ `-0` to `+0`, and the relational order is total. `interval(f64)` is the
354
+ parameterized dense intervalhalf-open, NaN-free, strictly ordered.
355
+ `span` builds checked interval values as `Result`s.
1593
356
 
1594
357
  ```ts
1595
- const created = await Db.create("./courses.db", KeyedRead)
1596
- if (created.tag !== "accepted") {
1597
- throw new Error("create rejected")
1598
- }
1599
- const db = created.value
1600
-
1601
- const minted: { grp?: bigint } = {}
1602
- db.write((tx) => {
1603
- const g = tx.reserve(Grp, "id", 1n).at(0n)!
1604
- tx.insert(Grp, [{ id: g, label: "algebra" }])
1605
- const course = tx.reserve(Course, "id", 1n).at(0n)!
1606
- tx.insert(Course, [{ id: course, grp: g, title: "linear equations" }])
1607
- minted.grp = g
1608
- })
1609
- const grp = minted.grp ?? 0n
358
+ const Window = relation("Window", { id: uuid, confidence: interval(f64), during: interval(i64) })
359
+ const Windows = schema("Windows", { Window }, [key(Window, ["id"])])
1610
360
 
1611
- // db.get the standalone keyed read through the declared law:
1612
- const byGroup = db.read((i) => i.get(Course, courseGrpKey, { grp }))
1613
-
1614
- // snap.get — the same spelling inside a read scope:
1615
- const viaSnap = db.read((instance) => instance.get(Course, courseGrpKey, { grp }))
1616
-
1617
- // tx.get — key-shaped read-modify-write, final-state (recipe 20's third
1618
- // idiom): per-fact premises need no earlier snapshot witness.
1619
- db.write((tx) => {
1620
- const current = tx.get(Course, courseGrpKey, { grp })
1621
- if (current !== undefined) {
1622
- tx.delete(Course, [current])
1623
- tx.insert(Course, [{ id: current.id, grp: current.grp, title: "linear equations II" }])
1624
- }
1625
- })
1626
-
1627
- // The primary 2-arg form — the fresh field is the primary key:
1628
- const byId = byGroup === undefined ? undefined : db.read((i) => i.get(Course, { id: byGroup.id }))
361
+ const discrete = span(0n, 60n)
362
+ const dense = span(0.25, 1.5)
363
+ const bothChecked: boolean = Result.isSuccess(discrete) && Result.isSuccess(dense)
364
+ void [Windows, bothChecked]
1629
365
  ```
1630
366
 
1631
- The anti-pattern this recipe retires: a scan-and-find where a key law
1632
- exists — `snap.scan(Course).find((row) => row.grp === grp)` — re-derives
1633
- in the host what the store already enforces. The uniqueness the fold
1634
- quietly assumes IS the declared key statement; spell the law and the point
1635
- read comes with it.
1636
-
1637
- ## Capacity laws
1638
-
1639
- ## 31. The power budget
367
+ ## 12. Scoped ownership and honest close
1640
368
 
1641
- Guarantee: Lean theorem + validator/runtime premises the capacity law
1642
- bounds each pool's summed draw by the pool's own row
1643
- (`lean/Bumbledb/Capacity.lean: CapacityLaw`; per touched parent, one keyed
1644
- probe plus one measure walk, `lean/Bumbledb/Oracle.lean:
1645
- capacity_plan_decides`); the pinned-column containment proves a device's
1646
- watts equals its model's at every commit.
1647
-
1648
- Per-group capacity is one statement — the `capacity` builder mirrors the
1649
- operator positionally (target, weight, window, source): `weigh` names the
1650
- measure on the SOURCE row, and `ref` reads each group's ceiling from the
1651
- TARGET row (hi slot only — ruled 2026-07-24, C6; bound names resolve
1652
- against the target's full roster, ruled 2026-07-24, C1).
369
+ Every native resource is scoped; early `close()` is itself an Effect
370
+ returning the honest `CloseReport`. A scope finalizer that cannot complete
371
+ teardown surfaces a structured `CloseFailure` DEFECT in the Cause never a
372
+ silently swallowed failure, never falsely reclaimed resources.
1653
373
 
1654
374
  ```ts
1655
- const Pool = relation("Pool", { id: u64.fresh, supply: u64 })
1656
- const Model = relation("Model", { id: u64.fresh, watts: u64 })
1657
- const Device = relation("Device", {
1658
- id: u64.fresh,
1659
- pool: u64,
1660
- model: u64,
1661
- watts: u64
1662
- })
375
+ const Item = relation("Item", { id: uuid, label: str })
376
+ const Items = schema("Items", { Item }, [key(Item, ["id"])])
1663
377
 
1664
- const Racks = schema("Racks", { Pool, Model, Device }, [
1665
- contained(on(Device, "pool"), on(Pool, "id")),
1666
- // The pinned column: a device's watts provably equals its model's — the
1667
- // two-column containment IS the join, stated as a law. The superkey it
1668
- // targets is deliberate write-amplification rent.
1669
- key(Model, ["id", "watts"]),
1670
- contained(on(Device, ["model", "watts"]), on(Model, ["id", "watts"])),
1671
- // Σ watts over a pool's devices stays within the pool's own supply:
1672
- capacity(on(Pool, "id"), weigh("watts"), within(0n, ref("supply")), on(Device, "pool"))
1673
- ])
1674
-
1675
- // utilization is a query, never a column (the ledger's law, recipe 19):
1676
- const draw = query(Racks).rule((r) => {
1677
- const { id, pool, watts } = v(Device)
1678
- return r.match(Device, { id, pool, watts }).find({ pool, total: r.sum(watts) })
1679
- })
378
+ const explicitClose = Effect.scoped(
379
+ Effect.gen(function* () {
380
+ const db = yield* Db.open(localPath, Items, work)
381
+ const report = yield* db.close()
382
+ // `closed` releases this capability's obligations; `incomplete` and
383
+ // `failed` retain native Closing accounting — they are never
384
+ // counted as reclaimed.
385
+ return report.kind
386
+ })
387
+ )
388
+ void explicitClose
1680
389
  ```
1681
390
 
1682
- A path weight (`weigh("model.watts")`) is a typed refusal whose diagnostic
1683
- names exactly this pair — the weight vocabulary is closed at the row (ruled
1684
- 2026-07-24, ruling 6): a weight read through a reference would be a
1685
- maintained copy of another relation's field, and a catalog edit would
1686
- silently re-weigh deployed fleets. Pinned, the inconsistent commit refuses
1687
- at the device site and the migration is explicit.
1688
-
1689
- ## 32. Calendar capacity
391
+ ## 13. Unresolved field arithmetic is authoring metadata
1690
392
 
1691
- Guarantee: Lean theorem + validator/runtime premises the Duration weight
1692
- sums each booking's interval measure against the room's own span measure
1693
- (`lean/Bumbledb/Capacity.lean: CapacityLaw`; Duration weights pair with
1694
- Duration-capable bounds, ruled 2026-07-24, C18; a ray-valued weight or
1695
- bound refuses typed at the law site, ruled 2026-07-24, C10).
1696
-
1697
- "Total booked time per room stays within the room's span" — one statement.
1698
- The interval enters through the measure argument, never the group key.
393
+ `Scalar.field("units")` is not a typed program. Builders accept it inside
394
+ arithmetic. Native compilation binds it against the verified source
395
+ schema including empty input — before any manifest write or freeze.
1699
396
 
1700
397
  ```ts
1701
- const Room = relation("Room", { id: u64.fresh, span: interval(i64) })
1702
- const Booking = relation("Booking", {
1703
- id: u64.fresh,
1704
- room: u64,
1705
- booked: interval(i64)
1706
- })
1707
-
1708
- const Rooms = schema("Rooms", { Room, Booking }, [
1709
- contained(on(Booking, "room"), on(Room, "id")),
1710
- // The pointwise key forbids double-booking (recipe 1); the capacity law
1711
- // bounds the TOTAL. Different laws — a schema usually wants both.
1712
- key(Booking, ["room", "booked"]),
1713
- capacity(
1714
- on(Room, "id"),
1715
- weigh(duration("booked")),
1716
- within(0n, duration("span")),
1717
- on(Booking, "room")
1718
- )
1719
- ])
1720
-
1721
- // the booked time per room is host arithmetic on the `booked`
1722
- // endpoints every answer row already carries (`end − start`).
398
+ const incrementUnits = Scalar.add(Scalar.field("units"), Scalar.u64(1n))
399
+ const asFloat = Scalar.toF64(Scalar.add(Scalar.field("units"), Scalar.u64(1n)))
400
+ void [incrementUnits, asFloat]
1723
401
  ```
1724
-
1725
- Mind the weighted `{0}` and the weighted floor: on a weighted statement
1726
- `within(0n)` says "the group's total is zero" (zero-measure rows may exist
1727
- — the weaker law than the unit exclusion), and `within(1n, "*")` under a
1728
- weight ("positive total") is not "at least one booking" — that intent is
1729
- the bare `contained`. Choose by what you mean.